@desplega.ai/agent-swarm 2.4.2 → 2.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{acp-adapter-dv7hmx4r.js → acp-adapter-thq04adh.js} +4 -5
- package/dist/{actions-51nc1gny.js → actions-j6ywh6x5.js} +18 -18
- package/dist/{amp-adapter-abp5tdx8.js → amp-adapter-k8b5xmj8.js} +3 -3
- package/dist/{app-9h14qvr6.js → app-rb7vc02s.js} +3 -3
- package/dist/{assistant-wv1gz3mt.js → assistant-ftta1zjf.js} +20 -20
- package/dist/{boot-reembed-6yn53ms9.js → boot-reembed-srp6v6af.js} +1 -1
- package/dist/{boot-reembed-7fcka4z5.js → boot-reembed-xns7h5h9.js} +1 -1
- package/dist/{boot-scrub-sweep-pm1580ax.js → boot-scrub-sweep-3qqrh2mz.js} +3 -3
- package/dist/{boot-seeds-g5hfc1x8.js → boot-seeds-9ks8hce7.js} +4 -4
- package/dist/{claude-adapter-gf3kxhf0.js → claude-adapter-k5ksyres.js} +4 -4
- package/dist/{claude-managed-setup-h00hsgtz.js → claude-managed-setup-n5a5eqks.js} +1 -1
- package/dist/{claude-sdk-session-s2k1qd8y.js → claude-sdk-session-2j3j4kwn.js} +9822 -9749
- package/dist/{cli-pkht5hap.js → cli-1d70crsy.js} +2 -2
- package/dist/{cli-r99kc9yq.js → cli-1ephgwnq.js} +3 -3
- package/dist/{cli-7225gwj7.js → cli-1m17dc9d.js} +1 -1
- package/dist/{cli-bcj753sq.js → cli-2v045xyf.js} +3 -3
- package/dist/{cli-h38rjv87.js → cli-2y91wnbh.js} +12 -7
- package/dist/{cli-sv5gb0yb.js → cli-2zqpkvtt.js} +1 -1
- package/dist/{cli-xbzvet96.js → cli-36ckw0wx.js} +5 -4
- package/dist/{cli-8sxqsbbq.js → cli-37yb7307.js} +1 -1
- package/dist/{cli-hyfw2j1e.js → cli-3cgngyd8.js} +11 -11
- package/dist/{cli-a4gvwttf.js → cli-44159k28.js} +2 -2
- package/dist/{cli-7awf1fp8.js → cli-4dg55wsk.js} +3 -3
- package/dist/{cli-mhywgrrz.js → cli-4sydckyj.js} +1 -1
- package/dist/{cli-y2x52gs2.js → cli-51y09c6p.js} +20 -17
- package/dist/{cli-ahtpy9kf.js → cli-55qmbp6z.js} +10 -5
- package/dist/{cli-pw7v69vp.js → cli-625tb7aq.js} +1 -1
- package/dist/{cli-x5mq1fyq.js → cli-74c1wpm0.js} +3 -3
- package/dist/{cli-b1dfr1m4.js → cli-7a6p4e65.js} +1 -1
- package/dist/{cli-hqhzg1qq.js → cli-82d3nkc3.js} +1 -1
- package/dist/{cli-nmqdc2t3.js → cli-9fhkkpj5.js} +78 -12
- package/dist/{cli-h93c9vb3.js → cli-amyhm3gj.js} +1 -1
- package/dist/{cli-0qvtffe9.js → cli-b8k7epj1.js} +6 -6
- package/dist/{cli-1pa3rz69.js → cli-ba8ne1es.js} +6 -1
- package/dist/{cli-g0hgtz4p.js → cli-bnz0r3kc.js} +3 -3
- package/dist/{cli-0hyhdkdk.js → cli-cebvbc9t.js} +4 -1
- package/dist/{cli-efaqv1c7.js → cli-cynvc9fj.js} +35 -7
- package/dist/{cli-8zwe9mpk.js → cli-dqb4n629.js} +10 -10
- package/dist/{cli-7a91w22y.js → cli-e8xyjvfc.js} +1 -1
- package/dist/{cli-s6vr7r2d.js → cli-esxerzg7.js} +1 -1
- package/dist/{cli-w9g4sz81.js → cli-gf2850q7.js} +1 -1
- package/dist/{cli-10hhg0km.js → cli-gqvh8d8a.js} +7 -7
- package/dist/{cli-t30yj95q.js → cli-hj3debgw.js} +1 -1
- package/dist/{cli-fpmsqq59.js → cli-nc35pxb7.js} +3 -3
- package/dist/{cli-d3qqdy4c.js → cli-p20r8w42.js} +16 -14
- package/dist/{cli-shvfedvm.js → cli-pccfmtb5.js} +5 -5
- package/dist/{cli-02qm7src.js → cli-q6r7t299.js} +1037 -299
- package/dist/{cli-2wag8973.js → cli-qeaq3j6t.js} +1 -1
- package/dist/{cli-nn47d7e8.js → cli-qkhc5aqy.js} +15 -15
- package/dist/{cli-faypxz7q.js → cli-t7cyef5g.js} +4 -4
- package/dist/{cli-z4wd2ftv.js → cli-te6b5wvg.js} +149 -184
- package/dist/{cli-5rk36sjf.js → cli-v3yr2gbg.js} +119 -44
- package/dist/{cli-239yjk27.js → cli-vdm20wej.js} +5 -5
- package/dist/{cli-zfg9enh9.js → cli-vfha21c9.js} +6 -1
- package/dist/{cli-rdyw3mct.js → cli-vq97g1jq.js} +6 -5
- package/dist/{cli-n85dqg6d.js → cli-w6nadkh4.js} +4 -4
- package/dist/{cli-99jez72r.js → cli-w9etnnxk.js} +6 -1
- package/dist/{cli-saw9a6f6.js → cli-xc27fwzg.js} +2 -2
- package/dist/{cli-ftyypbg5.js → cli-xj0re5tm.js} +1 -1
- package/dist/{cli-fyzjmseb.js → cli-zrk6jxv8.js} +1 -1
- package/dist/{cli-y8n5k515.js → cli-zrwpctp4.js} +28 -28
- package/dist/cli.js +44 -19
- package/dist/{codex-adapter-ttygw382.js → codex-adapter-968xwfy7.js} +2 -2
- package/dist/{codex-hook-x7nejyrr.js → codex-hook-e2he7aca.js} +2 -2
- package/dist/{codex-session-runner-60b58vv3.js → codex-session-runner-ht4z99fa.js} +2 -2
- package/dist/{commands-edmdwa4x.js → commands-pcymaeg9.js} +3 -3
- package/dist/{controller-m263fg43.js → controller-fpjyk46r.js} +3 -3
- package/dist/{controllers-connect-5fym8sw1.js → controllers-connect-mwzx1fh6.js} +19 -19
- package/dist/{cursor-adapter-cz6nm6mj.js → cursor-adapter-cp4s2e2f.js} +2 -2
- package/dist/{db-2y0mgstc.js → db-sgd12bt9.js} +3 -3
- package/dist/{db-nn0hcmx5.js → db-xsjg9jqq.js} +2 -2
- package/dist/{dispatcher-0a9wwjcx.js → dispatcher-x0jfw2c9.js} +17 -17
- package/dist/{dsh-adapter-3n3net33.js → dsh-adapter-jrq7ma7h.js} +2 -2
- package/dist/{e2b-thdcxtjd.js → e2b-16d6ch39.js} +1 -1
- package/dist/{e2b-7am6tmq6.js → e2b-zn0b4w5w.js} +1 -1
- package/dist/{external-channel-zhvt95n3.js → external-channel-8njj4h3b.js} +2 -2
- package/dist/{grok-adapter-qnr1hp2y.js → grok-adapter-kkr97v2f.js} +5 -6
- package/dist/handlers-0pvee9nw.js +39 -0
- package/dist/{heartbeat-gz97r7ac.js → heartbeat-wd03zhn0.js} +20 -20
- package/dist/{hook-1mmdgjqs.js → hook-f0vghj42.js} +10 -5
- package/dist/{hook-x8rthca4.js → hook-xzj2ts2p.js} +2 -2
- package/dist/{http-r8f85xr9.js → http-4hwxv62p.js} +261 -123
- package/dist/{index-va3y63es.js → index-6srwvd5b.js} +1 -2
- package/dist/{index-y7rbcrc1.js → index-97q4xn03.js} +20 -11
- package/dist/{index-s98037v0.js → index-cbxkmfc5.js} +1 -2
- package/dist/{index-vzvfqzfj.js → index-gdh8jrdm.js} +17 -17
- package/dist/{index-aex0eba9.js → index-hg0x2cdv.js} +17 -17
- package/dist/index-thm0y6a8.js +41 -0
- package/dist/{index-smxyk04s.js → index-ww2vznkp.js} +1 -1
- package/dist/{keepalive-64c0f34c.js → keepalive-hrh7xnr9.js} +3 -3
- package/dist/{lead-b1da0f97.js → lead-ee8vazz2.js} +30 -31
- package/dist/{maintenance-mnxpar12.js → maintenance-e8wnwnqx.js} +1 -1
- package/dist/{oauth-refresh-sweep-36hs2v4h.js → oauth-refresh-sweep-zm0yj4sj.js} +2 -2
- package/dist/{onboard-xes8de61.js → onboard-dtwcq603.js} +3 -3
- package/dist/{opencode-adapter-3s7ax02e.js → opencode-adapter-x4tqzhkh.js} +3 -3
- package/dist/{otel-impl-fzc57kcq.js → otel-impl-w3fbhpn0.js} +1 -1
- package/dist/{pi-mono-adapter-ytp7gz8b.js → pi-mono-adapter-mcfkn9vf.js} +3 -3
- package/dist/{pricing-refresh-1nek2w7b.js → pricing-refresh-j38dehfs.js} +3 -3
- package/dist/{rbac-roles-ttyfaep2.js → rbac-roles-ae8yttq4.js} +3 -3
- package/dist/{rbac-roles-sqefb5hw.js → rbac-roles-ejxx9b35.js} +2 -2
- package/dist/{render-v2-vb527jwn.js → render-v2-n16n0phe.js} +3 -3
- package/dist/{resume-jcdzg1bn.js → resume-z0fgjc38.js} +5 -5
- package/dist/{session-access-zsp4wsp0.js → session-access-cv3mq6mc.js} +6 -4
- package/dist/{setup-1nb2zqd3.js → setup-mafhj3yt.js} +1 -1
- package/dist/{work-objects-61w08fwr.js → work-objects-8b86mgjp.js} +21 -21
- package/dist/{worker-c1e68hpa.js → worker-16s75hm5.js} +30 -31
- package/dist/worker-token-xf11yyds.js +136 -0
- package/openapi.json +295 -12
- package/package.json +5 -5
- package/src/apps/action-runner.ts +4 -0
- package/src/apps/catalog.generated.json +3 -0
- package/src/apps/page-validator.ts +8 -2
- package/src/be/db.ts +7 -4
- package/src/be/migrations/231_slack_tasks_last_updated_index.sql +5 -0
- package/src/be/migrations/232_agent_session_tokens.sql +41 -0
- package/src/be/seed-scripts/catalog/heartbeat-jev-gate.ts +10 -1
- package/src/be/swarm-config-guard.ts +16 -0
- package/src/be/users.ts +169 -12
- package/src/cli.tsx +24 -0
- package/src/commands/worker-token.ts +164 -0
- package/src/github/handlers.ts +6 -5
- package/src/hooks/hook.ts +2 -1
- package/src/http/apps.ts +43 -5
- package/src/http/codex-oauth-keep-warm.ts +27 -9
- package/src/http/config.ts +5 -3
- package/src/http/core.ts +2 -0
- package/src/http/session-access.ts +51 -2
- package/src/http/sessions.ts +249 -10
- package/src/http/tasks.ts +4 -32
- package/src/providers/acp-adapter.ts +33 -13
- package/src/providers/acp-swarm-events.ts +1 -1
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/codex-adapter.ts +4 -4
- package/src/providers/codex-oauth/storage.ts +1 -1
- package/src/providers/opencode-adapter.ts +2 -2
- package/src/providers/pi-mono-extension.ts +2 -1
- package/src/rbac/legacy-policy.ts +3 -1
- package/src/rbac/permissions.ts +4 -0
- package/src/tests/acp-adapter.test.ts +10 -23
- package/src/tests/acp-fake-agent.ts +36 -0
- package/src/tests/acp-swarm-events.test.ts +1 -1
- package/src/tests/agent-session-tokens.test.ts +452 -0
- package/src/tests/apps-action-parent-task.test.ts +295 -0
- package/src/tests/claude-stop-hook.test.ts +47 -0
- package/src/tests/codex-adapter.test.ts +42 -0
- package/src/tests/codex-oauth-keep-warm.test.ts +85 -1
- package/src/tests/config-session-auth.test.ts +14 -0
- package/src/tests/docs-index-footprint.test.js +2 -2
- package/src/tests/env-flag.test.ts +15 -1
- package/src/tests/github-handlers-cancel-config.test.ts +29 -8
- package/src/tests/github-handlers-inline-comments.test.ts +9 -2
- package/src/tests/grok-adapter.test.ts +3 -3
- package/src/tests/heartbeat-jev-gate.test.ts +23 -0
- package/src/tests/migration-232-agent-session-tokens.test.ts +67 -0
- package/src/tests/rbac-engine.test.ts +1 -0
- package/src/tests/runtime-child-token-cleanup.test.ts +3 -3
- package/src/tests/script-connections.test.ts +4 -0
- package/src/tests/session-route-posture.test.ts +2 -0
- package/src/tests/skill-script-refs.test.ts +63 -0
- package/src/tests/slack-render-v2-query-plans.test.ts +147 -0
- package/src/tests/worker-token-cli.test.ts +76 -0
- package/src/tests/workflow-executors.test.ts +4 -2
- package/src/tests/workflow-system-one-extraction.test.ts +805 -0
- package/src/types.ts +2 -1
- package/src/utils/env-flag.ts +9 -0
- package/src/utils/request-auth-context.ts +2 -1
- package/src/workflows/definition.ts +7 -1
- package/src/workflows/engine.ts +18 -12
- package/src/workflows/executors/index.ts +1 -0
- package/src/workflows/executors/registry.ts +2 -0
- package/src/workflows/executors/system-one-decision.ts +56 -255
- package/src/workflows/executors/system-one-extract-wire.ts +57 -0
- package/src/workflows/executors/system-one-extraction.ts +830 -0
- package/src/workflows/executors/system-one-providers.ts +277 -110
- package/src/workflows/executors/system-one-transport.ts +264 -0
- package/templates/official/lead/HEARTBEAT.md +1 -1
- package/templates/skills/apps/SKILL.md +4 -1
- package/templates/skills/apps/content.md +4 -1
- package/templates/skills/code-quality/content.md +9 -14
- package/templates/skills/heartbeat-runbook/content.md +3 -3
- package/dist/cli-v1sjhn52.js +0 -7
- package/dist/handlers-dkcarkd0.js +0 -39
- package/dist/index-5jecgp3n.js +0 -41
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
|
+
import { unlink } from "node:fs/promises";
|
|
3
|
+
import {
|
|
4
|
+
createServer as createHttpServer,
|
|
5
|
+
type IncomingMessage,
|
|
6
|
+
type Server,
|
|
7
|
+
type ServerResponse,
|
|
8
|
+
} from "node:http";
|
|
9
|
+
import { parseAppDefinition } from "../apps/definition";
|
|
10
|
+
import { createApp } from "../apps/store";
|
|
11
|
+
import {
|
|
12
|
+
closeDb,
|
|
13
|
+
createAgent,
|
|
14
|
+
createTask,
|
|
15
|
+
createTaskExtended,
|
|
16
|
+
createUser,
|
|
17
|
+
getTaskById,
|
|
18
|
+
initDb,
|
|
19
|
+
} from "../be/db";
|
|
20
|
+
import { type IdentityActor, mintToken } from "../be/users";
|
|
21
|
+
import { handleApps } from "../http/apps";
|
|
22
|
+
import { resolveHttpRequestAuth } from "../http/auth";
|
|
23
|
+
import { SESSION_WRITE_DENIED } from "../http/session-access";
|
|
24
|
+
import { getPathSegments, parseQueryParams } from "../http/utils";
|
|
25
|
+
import { setRequestAuth } from "../utils/request-auth-context";
|
|
26
|
+
import { listenOnFreePort } from "./test-net";
|
|
27
|
+
|
|
28
|
+
const TEST_DB_PATH = `/private/tmp/test-apps-action-parent-task-${process.pid}.sqlite`;
|
|
29
|
+
const API_KEY = "example-apps-action-parent-task-key";
|
|
30
|
+
const OPERATOR_ACTOR: IdentityActor = { kind: "operator", id: "apps-action-parent-task-test" };
|
|
31
|
+
/** Non-lead worker: the default caller. */
|
|
32
|
+
const AGENT_ID = crypto.randomUUID();
|
|
33
|
+
const LEAD_ID = crypto.randomUUID();
|
|
34
|
+
/** Owns the parent task the worker has no context for. */
|
|
35
|
+
const OTHER_AGENT_ID = crypto.randomUUID();
|
|
36
|
+
const TASK_ACTION = "generateScreen";
|
|
37
|
+
const SYNC_ACTION = "syncNotes";
|
|
38
|
+
const CHAIN_ISSUE = "$row, $rowIndex, $form, and $chain bindings are only allowed in action params";
|
|
39
|
+
|
|
40
|
+
let server: Server;
|
|
41
|
+
let base = "";
|
|
42
|
+
/** A user with no share on anyone else's session. */
|
|
43
|
+
let userToken = "";
|
|
44
|
+
let previousApiKey: string | undefined;
|
|
45
|
+
|
|
46
|
+
function createTestServer(): Server {
|
|
47
|
+
return createHttpServer(async (req: IncomingMessage, res: ServerResponse) => {
|
|
48
|
+
setRequestAuth(req, await resolveHttpRequestAuth(req, API_KEY));
|
|
49
|
+
res.setHeader("Content-Type", "application/json");
|
|
50
|
+
const pathSegments = getPathSegments(req.url || "");
|
|
51
|
+
const queryParams = parseQueryParams(req.url || "");
|
|
52
|
+
const myAgentId = req.headers["x-agent-id"] as string | undefined;
|
|
53
|
+
const handled = await handleApps(req, res, pathSegments, queryParams, myAgentId);
|
|
54
|
+
if (!handled) {
|
|
55
|
+
res.writeHead(404);
|
|
56
|
+
res.end(JSON.stringify({ error: "not found" }));
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Runs an action as an agent (default: the worker) or, with `bearer`, as a token holder. */
|
|
62
|
+
async function runAction<T>(
|
|
63
|
+
appId: string,
|
|
64
|
+
body: Record<string, unknown>,
|
|
65
|
+
options: { agentId?: string; bearer?: string; action?: string } = {},
|
|
66
|
+
): Promise<{ status: number; body: T }> {
|
|
67
|
+
const action = options.action ?? TASK_ACTION;
|
|
68
|
+
const response = await fetch(`${base}/api/apps/${appId}/actions/${action}`, {
|
|
69
|
+
method: "POST",
|
|
70
|
+
headers: {
|
|
71
|
+
"Content-Type": "application/json",
|
|
72
|
+
...(options.bearer
|
|
73
|
+
? { Authorization: `Bearer ${options.bearer}` }
|
|
74
|
+
: { "X-Agent-ID": options.agentId ?? AGENT_ID }),
|
|
75
|
+
},
|
|
76
|
+
body: JSON.stringify(body),
|
|
77
|
+
});
|
|
78
|
+
return { status: response.status, body: (await response.json()) as T };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** A page whose Button chains a task-kind action under the chain scope's leaf task. */
|
|
82
|
+
function chainedButtonPage() {
|
|
83
|
+
return {
|
|
84
|
+
root: "root",
|
|
85
|
+
elements: {
|
|
86
|
+
root: { type: "Container", props: {}, children: ["send"] },
|
|
87
|
+
send: {
|
|
88
|
+
type: "Button",
|
|
89
|
+
props: { label: "Send" },
|
|
90
|
+
on: {
|
|
91
|
+
press: [
|
|
92
|
+
{
|
|
93
|
+
action: "app.action",
|
|
94
|
+
params: {
|
|
95
|
+
name: TASK_ACTION,
|
|
96
|
+
input: { prompt: "next step" },
|
|
97
|
+
parentTaskId: { $chain: "leafTaskId" },
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function definitionWith(page: Record<string, unknown>) {
|
|
108
|
+
return {
|
|
109
|
+
models: {
|
|
110
|
+
note: {
|
|
111
|
+
columns: { title: { kind: "string" } },
|
|
112
|
+
// The sync action needs one (model x source) pair to be valid.
|
|
113
|
+
sources: { tracker: { connector: "swarm-tasks", joinKey: "title" } },
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
queries: { allNotes: { model: "note" } },
|
|
117
|
+
actions: {
|
|
118
|
+
[TASK_ACTION]: { kind: "task", prompt: "Generate the next screen" },
|
|
119
|
+
// A non-task action, to show parentTaskId is rejected rather than dropped.
|
|
120
|
+
[SYNC_ACTION]: { kind: "sync" },
|
|
121
|
+
},
|
|
122
|
+
pages: { main: page },
|
|
123
|
+
defaultPage: "main",
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Stores the chained-Button app the way the API does: validated first. */
|
|
128
|
+
async function createChainedApp(): Promise<string> {
|
|
129
|
+
const parsed = await parseAppDefinition(definitionWith(chainedButtonPage()));
|
|
130
|
+
if (!parsed.success) throw new Error(JSON.stringify(parsed.issues));
|
|
131
|
+
const app = await createApp({
|
|
132
|
+
name: `Chained ${crypto.randomUUID()}`,
|
|
133
|
+
definition: parsed.definition,
|
|
134
|
+
});
|
|
135
|
+
return app.id;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
beforeAll(async () => {
|
|
139
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
140
|
+
try {
|
|
141
|
+
await unlink(`${TEST_DB_PATH}${suffix}`);
|
|
142
|
+
} catch {}
|
|
143
|
+
}
|
|
144
|
+
// Safe to mutate: CI runs one process per test file (`bun test --parallel=4`)
|
|
145
|
+
// and afterAll restores the previous value.
|
|
146
|
+
previousApiKey = process.env.AGENT_SWARM_API_KEY;
|
|
147
|
+
process.env.AGENT_SWARM_API_KEY = API_KEY;
|
|
148
|
+
initDb(TEST_DB_PATH);
|
|
149
|
+
const user = await createUser({ name: "Parent Task User" });
|
|
150
|
+
userToken = (await mintToken(user.id, "apps-action-parent-task", OPERATOR_ACTOR)).plaintext;
|
|
151
|
+
await createAgent({ id: AGENT_ID, name: "parent-task-worker", isLead: false, status: "idle" });
|
|
152
|
+
await createAgent({ id: LEAD_ID, name: "parent-task-lead", isLead: true, status: "idle" });
|
|
153
|
+
await createAgent({
|
|
154
|
+
id: OTHER_AGENT_ID,
|
|
155
|
+
name: "parent-task-other",
|
|
156
|
+
isLead: false,
|
|
157
|
+
status: "idle",
|
|
158
|
+
});
|
|
159
|
+
server = createTestServer();
|
|
160
|
+
const port = await listenOnFreePort(server, "127.0.0.1");
|
|
161
|
+
base = `http://127.0.0.1:${port}`;
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
afterAll(async () => {
|
|
165
|
+
await new Promise<void>((resolve) => server.close(() => resolve()));
|
|
166
|
+
closeDb();
|
|
167
|
+
if (previousApiKey === undefined) delete process.env.AGENT_SWARM_API_KEY;
|
|
168
|
+
else process.env.AGENT_SWARM_API_KEY = previousApiKey;
|
|
169
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
170
|
+
try {
|
|
171
|
+
await unlink(`${TEST_DB_PATH}${suffix}`);
|
|
172
|
+
} catch {}
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
describe("$chain sentinel in the page validator", () => {
|
|
177
|
+
test("accepts parentTaskId: { $chain } inside an app.action step's params", async () => {
|
|
178
|
+
const parsed = await parseAppDefinition(definitionWith(chainedButtonPage()));
|
|
179
|
+
expect(parsed.success).toBe(true);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test("accepts a $chain sentinel nested deeper inside app.action params", async () => {
|
|
183
|
+
const page = chainedButtonPage();
|
|
184
|
+
page.elements.send.on.press[0]!.params = {
|
|
185
|
+
name: TASK_ACTION,
|
|
186
|
+
input: { context: { parentTaskId: { $chain: "leafTaskId" } } },
|
|
187
|
+
};
|
|
188
|
+
const parsed = await parseAppDefinition(definitionWith(page));
|
|
189
|
+
expect(parsed.success).toBe(true);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test("rejects a $chain sentinel used as a plain prop value", async () => {
|
|
193
|
+
const page = chainedButtonPage();
|
|
194
|
+
page.elements = {
|
|
195
|
+
...page.elements,
|
|
196
|
+
echo: { type: "Text", props: { content: { $chain: "leafTaskId" } } },
|
|
197
|
+
} as typeof page.elements;
|
|
198
|
+
page.elements.root.children = ["send", "echo"];
|
|
199
|
+
const parsed = await parseAppDefinition(definitionWith(page));
|
|
200
|
+
expect(parsed.success).toBe(false);
|
|
201
|
+
if (parsed.success) return;
|
|
202
|
+
expect(parsed.issues.some((entry) => entry.message === CHAIN_ISSUE)).toBe(true);
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
describe("POST /api/apps/:id/actions/:name with parentTaskId", () => {
|
|
207
|
+
test("chains the created task under the given parent", async () => {
|
|
208
|
+
const appId = await createChainedApp();
|
|
209
|
+
const parent = await createTask(AGENT_ID, "Previous turn of the chain");
|
|
210
|
+
|
|
211
|
+
const started = await runAction<{ ok: boolean; taskId: string; status: string }>(appId, {
|
|
212
|
+
input: { prompt: "next step" },
|
|
213
|
+
parentTaskId: parent.id,
|
|
214
|
+
});
|
|
215
|
+
expect(started.status).toBe(200);
|
|
216
|
+
expect(started.body).toMatchObject({ ok: true, status: "pending" });
|
|
217
|
+
|
|
218
|
+
const created = await getTaskById(started.body.taskId);
|
|
219
|
+
expect(created?.parentTaskId).toBe(parent.id);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test("answers 404 for an unknown parent instead of storing a dangling id", async () => {
|
|
223
|
+
const appId = await createChainedApp();
|
|
224
|
+
const missing = await runAction<{ error: string }>(appId, {
|
|
225
|
+
input: {},
|
|
226
|
+
parentTaskId: crypto.randomUUID(),
|
|
227
|
+
});
|
|
228
|
+
expect(missing.status).toBe(404);
|
|
229
|
+
expect(missing.body.error).toBe("parent task not found");
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test("denies an agent caller without context for the parent, as POST /api/tasks does", async () => {
|
|
233
|
+
const appId = await createChainedApp();
|
|
234
|
+
const foreign = await createTask(OTHER_AGENT_ID, "Someone else's thread");
|
|
235
|
+
|
|
236
|
+
const denied = await runAction<{ error: string }>(appId, {
|
|
237
|
+
input: {},
|
|
238
|
+
parentTaskId: foreign.id,
|
|
239
|
+
});
|
|
240
|
+
expect(denied.status).toBe(403);
|
|
241
|
+
expect(denied.body.error).toBe(`You don't have context for parent task ${foreign.id}.`);
|
|
242
|
+
|
|
243
|
+
// A lead keeps its `task.parent.attach` grant: the gate is about identity, not the route.
|
|
244
|
+
const asLead = await runAction<{ taskId: string }>(
|
|
245
|
+
appId,
|
|
246
|
+
{ input: {}, parentTaskId: foreign.id },
|
|
247
|
+
{ agentId: LEAD_ID },
|
|
248
|
+
);
|
|
249
|
+
expect(asLead.status).toBe(200);
|
|
250
|
+
expect((await getTaskById(asLead.body.taskId))?.parentTaskId).toBe(foreign.id);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test("denies a user caller without a write share on the parent's session", async () => {
|
|
254
|
+
const appId = await createChainedApp();
|
|
255
|
+
const owner = await createUser({ name: "Session Owner" });
|
|
256
|
+
const theirs = await createTaskExtended("Someone else's session", {
|
|
257
|
+
agentId: OTHER_AGENT_ID,
|
|
258
|
+
requestedByUserId: owner.id,
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
const denied = await runAction<{ error: string }>(
|
|
262
|
+
appId,
|
|
263
|
+
{ input: {}, parentTaskId: theirs.id },
|
|
264
|
+
{ bearer: userToken },
|
|
265
|
+
);
|
|
266
|
+
expect(denied.status).toBe(403);
|
|
267
|
+
expect(denied.body.error).toBe(SESSION_WRITE_DENIED);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
test("rejects parentTaskId on a non-task action instead of dropping it", async () => {
|
|
271
|
+
const appId = await createChainedApp();
|
|
272
|
+
const parent = await createTask(AGENT_ID, "Previous turn of the chain");
|
|
273
|
+
|
|
274
|
+
const rejected = await runAction<{ error: string }>(
|
|
275
|
+
appId,
|
|
276
|
+
{ input: {}, parentTaskId: parent.id },
|
|
277
|
+
{ action: SYNC_ACTION },
|
|
278
|
+
);
|
|
279
|
+
expect(rejected.status).toBe(400);
|
|
280
|
+
expect(rejected.body.error).toBe("parentTaskId is only supported for task-kind actions");
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
test("rejects an empty parentTaskId", async () => {
|
|
284
|
+
const appId = await createChainedApp();
|
|
285
|
+
const rejected = await runAction<{ error: string }>(appId, { input: {}, parentTaskId: "" });
|
|
286
|
+
expect(rejected.status).toBe(400);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
test("still creates a parentless task when parentTaskId is omitted", async () => {
|
|
290
|
+
const appId = await createChainedApp();
|
|
291
|
+
const started = await runAction<{ ok: boolean; taskId: string }>(appId, { input: {} });
|
|
292
|
+
expect(started.status).toBe(200);
|
|
293
|
+
expect((await getTaskById(started.body.taskId))?.parentTaskId ?? null).toBeNull();
|
|
294
|
+
});
|
|
295
|
+
});
|
|
@@ -371,6 +371,53 @@ describe("runStopHookSessionSummary", () => {
|
|
|
371
371
|
expect(consoleErrors.length).toBe(0);
|
|
372
372
|
});
|
|
373
373
|
|
|
374
|
+
test.each(["true", "TRUE"])("SKIP_SESSION_SUMMARY=%s → no runSummarize call", async (value) => {
|
|
375
|
+
const transcriptPath = await writeTempTranscript(longTranscript());
|
|
376
|
+
|
|
377
|
+
let runSummarizeCalled = false;
|
|
378
|
+
const deps: RunStopHookSessionSummaryDeps = {
|
|
379
|
+
runSummarize: async () => {
|
|
380
|
+
runSummarizeCalled = true;
|
|
381
|
+
throw new Error("should not be called");
|
|
382
|
+
},
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
await runStopHookSessionSummary(
|
|
386
|
+
{
|
|
387
|
+
agentId: "agent-claude-1",
|
|
388
|
+
transcriptPath,
|
|
389
|
+
env: makeEnv({ SKIP_SESSION_SUMMARY: value }),
|
|
390
|
+
},
|
|
391
|
+
deps,
|
|
392
|
+
);
|
|
393
|
+
|
|
394
|
+
expect(runSummarizeCalled).toBe(false);
|
|
395
|
+
expect(fetchCalls.length).toBe(0);
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
test.each(["0", "false"])("SKIP_SESSION_SUMMARY=%s → summary still runs", async (value) => {
|
|
399
|
+
const transcriptPath = await writeTempTranscript(longTranscript());
|
|
400
|
+
|
|
401
|
+
let runSummarizeCalled = false;
|
|
402
|
+
const deps: RunStopHookSessionSummaryDeps = {
|
|
403
|
+
runSummarize: async () => {
|
|
404
|
+
runSummarizeCalled = true;
|
|
405
|
+
return null;
|
|
406
|
+
},
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
await runStopHookSessionSummary(
|
|
410
|
+
{
|
|
411
|
+
agentId: "agent-claude-1",
|
|
412
|
+
transcriptPath,
|
|
413
|
+
env: makeEnv({ SKIP_SESSION_SUMMARY: value }),
|
|
414
|
+
},
|
|
415
|
+
deps,
|
|
416
|
+
);
|
|
417
|
+
|
|
418
|
+
expect(runSummarizeCalled).toBe(true);
|
|
419
|
+
});
|
|
420
|
+
|
|
374
421
|
test("short transcript (≤100 chars) → no runSummarize call, no POST", async () => {
|
|
375
422
|
const transcriptPath = await writeTempTranscript("tiny");
|
|
376
423
|
|
|
@@ -2294,6 +2294,48 @@ describe("CodexSession session-end summarization", () => {
|
|
|
2294
2294
|
expect(indexCalls.length).toBe(0);
|
|
2295
2295
|
});
|
|
2296
2296
|
|
|
2297
|
+
test.each(["true", "TRUE"])("SKIP_SESSION_SUMMARY=%s → no runSummarize call", async (value) => {
|
|
2298
|
+
process.env.SKIP_SESSION_SUMMARY = value;
|
|
2299
|
+
const events = buildSummaryEvents();
|
|
2300
|
+
|
|
2301
|
+
let runSummarizeCalls = 0;
|
|
2302
|
+
const deps: SummarizeSessionForCodexDeps = {
|
|
2303
|
+
runSummarize: async () => {
|
|
2304
|
+
runSummarizeCalls += 1;
|
|
2305
|
+
return null;
|
|
2306
|
+
},
|
|
2307
|
+
};
|
|
2308
|
+
|
|
2309
|
+
const config = testConfig({
|
|
2310
|
+
logFile: join(tmpLogDir, `skip-${value}.log`),
|
|
2311
|
+
cwd: "",
|
|
2312
|
+
});
|
|
2313
|
+
await runSessionWithFakeThreadAndDeps(events, config, deps);
|
|
2314
|
+
|
|
2315
|
+
expect(runSummarizeCalls).toBe(0);
|
|
2316
|
+
});
|
|
2317
|
+
|
|
2318
|
+
test.each(["0", "false"])("SKIP_SESSION_SUMMARY=%s → summary still runs", async (value) => {
|
|
2319
|
+
process.env.SKIP_SESSION_SUMMARY = value;
|
|
2320
|
+
const events = buildSummaryEvents();
|
|
2321
|
+
|
|
2322
|
+
let runSummarizeCalls = 0;
|
|
2323
|
+
const deps: SummarizeSessionForCodexDeps = {
|
|
2324
|
+
runSummarize: async () => {
|
|
2325
|
+
runSummarizeCalls += 1;
|
|
2326
|
+
return null;
|
|
2327
|
+
},
|
|
2328
|
+
};
|
|
2329
|
+
|
|
2330
|
+
const config = testConfig({
|
|
2331
|
+
logFile: join(tmpLogDir, `no-skip-${value}.log`),
|
|
2332
|
+
cwd: "",
|
|
2333
|
+
});
|
|
2334
|
+
await runSessionWithFakeThreadAndDeps(events, config, deps);
|
|
2335
|
+
|
|
2336
|
+
expect(runSummarizeCalls).toBe(1);
|
|
2337
|
+
});
|
|
2338
|
+
|
|
2297
2339
|
test("missing taskId → no runSummarize call, no POST", async () => {
|
|
2298
2340
|
const events = buildSummaryEvents();
|
|
2299
2341
|
|
|
@@ -10,7 +10,8 @@ import { afterAll, afterEach, beforeAll, describe, expect, it } from "bun:test";
|
|
|
10
10
|
import { unlink } from "node:fs/promises";
|
|
11
11
|
import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http";
|
|
12
12
|
import { closeDb, initDb, upsertKv } from "../be/db";
|
|
13
|
-
import {
|
|
13
|
+
import { validateConfigValue } from "../be/swarm-config-guard";
|
|
14
|
+
import { handleCodexOAuthKeepWarm, keepWarmMaxAgeMs } from "../http/codex-oauth-keep-warm";
|
|
14
15
|
import { handleOAuthLocks } from "../http/oauth-locks";
|
|
15
16
|
import { resetFetchForTesting, setFetchForTesting } from "../providers/codex-oauth/flow.js";
|
|
16
17
|
import type { CodexOAuthCredentials } from "../providers/codex-oauth/types.js";
|
|
@@ -176,6 +177,70 @@ describe("POST /api/oauth/keep-warm/codex", () => {
|
|
|
176
177
|
expect(slots.get(1)?.access).toBe("example-at_keepwarm");
|
|
177
178
|
});
|
|
178
179
|
|
|
180
|
+
it("refreshes a 6-day-old slot by default, before the ~6d20h point where OpenAI ended a session", async () => {
|
|
181
|
+
const slots = new Map<number, CodexOAuthCredentials>([
|
|
182
|
+
[
|
|
183
|
+
0,
|
|
184
|
+
{
|
|
185
|
+
access: "at_six_days",
|
|
186
|
+
refresh: "rt_six_days",
|
|
187
|
+
expires: Date.now() + 4 * 24 * 60 * 60 * 1000, // issued ~6 days ago (10d TTL)
|
|
188
|
+
accountId: "acc-six",
|
|
189
|
+
},
|
|
190
|
+
],
|
|
191
|
+
]);
|
|
192
|
+
installMockTransport(slots);
|
|
193
|
+
setFetchForTesting(
|
|
194
|
+
async () =>
|
|
195
|
+
new Response(
|
|
196
|
+
JSON.stringify({
|
|
197
|
+
access_token: "example-at_six_days_new",
|
|
198
|
+
refresh_token: "example-rt_six_days_new",
|
|
199
|
+
expires_in: 864000,
|
|
200
|
+
}),
|
|
201
|
+
{ status: 200, headers: { "Content-Type": "application/json" } },
|
|
202
|
+
),
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
const { req, res, captured } = fakeReqRes();
|
|
206
|
+
await handleCodexOAuthKeepWarm(req, res, ["api", "oauth", "keep-warm", "codex"]);
|
|
207
|
+
|
|
208
|
+
const body = JSON.parse(captured.body) as { results: Array<Record<string, unknown>> };
|
|
209
|
+
expect(body.results[0]).toMatchObject({ slot: 0, outcome: "refreshed" });
|
|
210
|
+
expect(slots.get(0)?.refresh).toBe("example-rt_six_days_new");
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("honors CODEX_OAUTH_KEEP_WARM_MAX_AGE_HOURS read at sweep time", async () => {
|
|
214
|
+
const saved = process.env.CODEX_OAUTH_KEEP_WARM_MAX_AGE_HOURS;
|
|
215
|
+
process.env.CODEX_OAUTH_KEEP_WARM_MAX_AGE_HOURS = "168";
|
|
216
|
+
try {
|
|
217
|
+
const slots = new Map<number, CodexOAuthCredentials>([
|
|
218
|
+
[
|
|
219
|
+
0,
|
|
220
|
+
{
|
|
221
|
+
access: "at_six_days",
|
|
222
|
+
refresh: "rt_six_days",
|
|
223
|
+
expires: Date.now() + 4 * 24 * 60 * 60 * 1000, // issued ~6 days ago, under 168h
|
|
224
|
+
accountId: "acc-six",
|
|
225
|
+
},
|
|
226
|
+
],
|
|
227
|
+
]);
|
|
228
|
+
installMockTransport(slots);
|
|
229
|
+
setFetchForTesting(async () => {
|
|
230
|
+
throw new Error("no /oauth/token exchange should happen under a 168h threshold");
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
const { req, res, captured } = fakeReqRes();
|
|
234
|
+
await handleCodexOAuthKeepWarm(req, res, ["api", "oauth", "keep-warm", "codex"]);
|
|
235
|
+
|
|
236
|
+
const body = JSON.parse(captured.body) as { results: Array<Record<string, unknown>> };
|
|
237
|
+
expect(body.results[0]).toMatchObject({ slot: 0, outcome: "warm" });
|
|
238
|
+
} finally {
|
|
239
|
+
if (saved === undefined) delete process.env.CODEX_OAUTH_KEEP_WARM_MAX_AGE_HOURS;
|
|
240
|
+
else process.env.CODEX_OAUTH_KEEP_WARM_MAX_AGE_HOURS = saved;
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
|
|
179
244
|
it("skips a slot with an active codex-auth-watch bench marker", async () => {
|
|
180
245
|
const keySuffix = "c-old"; // last 5 of accountId (no chatgpt_user_id in this fixture's access token)
|
|
181
246
|
await upsertKv({
|
|
@@ -284,6 +349,25 @@ describe("POST /api/oauth/keep-warm/codex", () => {
|
|
|
284
349
|
}
|
|
285
350
|
});
|
|
286
351
|
|
|
352
|
+
it("keepWarmMaxAgeMs defaults to 120h and ignores invalid or out-of-range values", () => {
|
|
353
|
+
const hour = 60 * 60 * 1000;
|
|
354
|
+
expect(keepWarmMaxAgeMs({})).toBe(120 * hour);
|
|
355
|
+
expect(keepWarmMaxAgeMs({ CODEX_OAUTH_KEEP_WARM_MAX_AGE_HOURS: " 72 " })).toBe(72 * hour);
|
|
356
|
+
expect(keepWarmMaxAgeMs({ CODEX_OAUTH_KEEP_WARM_MAX_AGE_HOURS: "216" })).toBe(216 * hour);
|
|
357
|
+
for (const bad of ["0", "217", "1.5", "-3", "abc", ""]) {
|
|
358
|
+
expect(keepWarmMaxAgeMs({ CODEX_OAUTH_KEEP_WARM_MAX_AGE_HOURS: bad })).toBe(120 * hour);
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
it("the config API rejects the values keepWarmMaxAgeMs would fall back from", () => {
|
|
363
|
+
const key = "CODEX_OAUTH_KEEP_WARM_MAX_AGE_HOURS";
|
|
364
|
+
expect(validateConfigValue(key, "1")).toBeNull();
|
|
365
|
+
expect(validateConfigValue(key, "216")).toBeNull();
|
|
366
|
+
expect(validateConfigValue(key, "0")).toContain("between 1 and 216");
|
|
367
|
+
expect(validateConfigValue(key, "217")).toContain("between 1 and 216");
|
|
368
|
+
expect(validateConfigValue(key, "1.5")).toContain("integer");
|
|
369
|
+
});
|
|
370
|
+
|
|
287
371
|
it("does not match unrelated paths", async () => {
|
|
288
372
|
const { req, res } = fakeReqRes();
|
|
289
373
|
const handled = await handleCodexOAuthKeepWarm(req, res, [
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
2
|
import { createServer, type Server } from "node:http";
|
|
3
3
|
import { closeDb, createAgent, initDb, upsertSwarmConfig } from "../be/db";
|
|
4
|
+
import { mintAgentToken, revokeAgentTokens } from "../be/users";
|
|
4
5
|
import { handleConfig } from "../http/config";
|
|
5
6
|
import { handleCore } from "../http/core";
|
|
6
7
|
import { handleSessions } from "../http/sessions";
|
|
@@ -116,6 +117,19 @@ describe("config HTTP session-token authorization", () => {
|
|
|
116
117
|
expect((await response.json()).configs[0].value).toBe("global");
|
|
117
118
|
});
|
|
118
119
|
|
|
120
|
+
test("agent token resolved config without agentId reads its own agent scope", async () => {
|
|
121
|
+
const token = await mintAgentToken(workerId);
|
|
122
|
+
try {
|
|
123
|
+
const response = await fetch(`${baseUrl}/api/config/resolved?includeSecrets=true`, {
|
|
124
|
+
headers: headers(token.plaintext),
|
|
125
|
+
});
|
|
126
|
+
expect(response.status).toBe(200);
|
|
127
|
+
expect(await response.text()).toContain(SECRET);
|
|
128
|
+
} finally {
|
|
129
|
+
await revokeAgentTokens(workerId);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
119
133
|
for (const method of ["PUT", "DELETE"] as const) {
|
|
120
134
|
test(`${method} cannot escalate a non-lead session by changing only X-Agent-ID`, async () => {
|
|
121
135
|
const token = await mint(workerId);
|
|
@@ -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(440);
|
|
107
|
+
expect(generatedKeys).toHaveLength(440);
|
|
108
108
|
expect(new Set(generatedKeys)).toEqual(new Set(operationKeys));
|
|
109
109
|
});
|
|
110
110
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
import { validateConfigValue } from "../be/swarm-config-guard";
|
|
3
|
-
import { isEnvFlagEnabled, parseEnvFlag } from "../utils/env-flag";
|
|
3
|
+
import { isEnvFlagEnabled, isSessionSummarySkipped, parseEnvFlag } from "../utils/env-flag";
|
|
4
4
|
|
|
5
5
|
describe("parseEnvFlag", () => {
|
|
6
6
|
test("accepts both truthy serializations", () => {
|
|
@@ -199,3 +199,17 @@ describe("swarm-config-guard: Configuration-page value validation", () => {
|
|
|
199
199
|
expect(validateConfigValue("SOME_RANDOM_KEY", "whatever")).toBeNull();
|
|
200
200
|
});
|
|
201
201
|
});
|
|
202
|
+
|
|
203
|
+
describe("isSessionSummarySkipped", () => {
|
|
204
|
+
test.each(["1", "true", "TRUE", " True "])("SKIP_SESSION_SUMMARY=%p skips", (value) => {
|
|
205
|
+
expect(isSessionSummarySkipped({ SKIP_SESSION_SUMMARY: value })).toBe(true);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test.each(["0", "false", "FALSE", ""])("SKIP_SESSION_SUMMARY=%p keeps the summary", (value) => {
|
|
209
|
+
expect(isSessionSummarySkipped({ SKIP_SESSION_SUMMARY: value })).toBe(false);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test("unset keeps the summary", () => {
|
|
213
|
+
expect(isSessionSummarySkipped({})).toBe(false);
|
|
214
|
+
});
|
|
215
|
+
});
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
* github.cancelOnUnassign — PR unassigned + issue unassigned
|
|
7
7
|
* github.cancelOnReviewRequestRemoved — PR review_request_removed
|
|
8
8
|
*
|
|
9
|
-
* Absent /
|
|
10
|
-
*
|
|
9
|
+
* Absent / empty / unrecognized values → cancel (default).
|
|
10
|
+
* Values "false" / "0" (case-insensitive, trimmed) → leave task untouched.
|
|
11
11
|
*/
|
|
12
12
|
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
13
13
|
import { unlink } from "node:fs/promises";
|
|
@@ -122,8 +122,15 @@ describe("PR unassigned — github.cancelOnUnassign", () => {
|
|
|
122
122
|
expect(await getTaskStatus(taskId)).toBe("failed");
|
|
123
123
|
});
|
|
124
124
|
|
|
125
|
-
test(
|
|
126
|
-
|
|
125
|
+
test.each([
|
|
126
|
+
"false",
|
|
127
|
+
"FALSE",
|
|
128
|
+
"False",
|
|
129
|
+
"0",
|
|
130
|
+
" false ",
|
|
131
|
+
" 0 ",
|
|
132
|
+
])("config = %j: unassign leaves task untouched", async (value) => {
|
|
133
|
+
await setConfigFlag("github.cancelOnUnassign", value);
|
|
127
134
|
const taskId = await seedTask(BASE_PR.number, "pr");
|
|
128
135
|
expect(await getTaskStatus(taskId)).toBe("pending");
|
|
129
136
|
|
|
@@ -162,8 +169,15 @@ describe("issue unassigned — github.cancelOnUnassign", () => {
|
|
|
162
169
|
expect(await getTaskStatus(taskId)).toBe("failed");
|
|
163
170
|
});
|
|
164
171
|
|
|
165
|
-
test(
|
|
166
|
-
|
|
172
|
+
test.each([
|
|
173
|
+
"false",
|
|
174
|
+
"FALSE",
|
|
175
|
+
"False",
|
|
176
|
+
"0",
|
|
177
|
+
" false ",
|
|
178
|
+
" 0 ",
|
|
179
|
+
])("config = %j: unassign leaves task untouched", async (value) => {
|
|
180
|
+
await setConfigFlag("github.cancelOnUnassign", value);
|
|
167
181
|
const taskId = await seedTask(BASE_ISSUE.number, "issue");
|
|
168
182
|
expect(await getTaskStatus(taskId)).toBe("pending");
|
|
169
183
|
|
|
@@ -201,8 +215,15 @@ describe("PR review_request_removed — github.cancelOnReviewRequestRemoved", ()
|
|
|
201
215
|
expect(await getTaskStatus(taskId)).toBe("failed");
|
|
202
216
|
});
|
|
203
217
|
|
|
204
|
-
test(
|
|
205
|
-
|
|
218
|
+
test.each([
|
|
219
|
+
"false",
|
|
220
|
+
"FALSE",
|
|
221
|
+
"False",
|
|
222
|
+
"0",
|
|
223
|
+
" false ",
|
|
224
|
+
" 0 ",
|
|
225
|
+
])("config = %j: review removal leaves task untouched", async (value) => {
|
|
226
|
+
await setConfigFlag("github.cancelOnReviewRequestRemoved", value);
|
|
206
227
|
const taskId = await seedTask(BASE_PR.number, "pr");
|
|
207
228
|
expect(await getTaskStatus(taskId)).toBe("pending");
|
|
208
229
|
|
|
@@ -573,9 +573,16 @@ describe("no-op approved reviews", () => {
|
|
|
573
573
|
}
|
|
574
574
|
});
|
|
575
575
|
|
|
576
|
-
test(
|
|
576
|
+
test.each([
|
|
577
|
+
"false",
|
|
578
|
+
"FALSE",
|
|
579
|
+
"False",
|
|
580
|
+
"0",
|
|
581
|
+
" false ",
|
|
582
|
+
" 0 ",
|
|
583
|
+
])("GITHUB_SKIP_NOOP_APPROVALS=%j: empty-body approval still creates a task", async (value) => {
|
|
577
584
|
const previous = process.env.GITHUB_SKIP_NOOP_APPROVALS;
|
|
578
|
-
process.env.GITHUB_SKIP_NOOP_APPROVALS =
|
|
585
|
+
process.env.GITHUB_SKIP_NOOP_APPROVALS = value;
|
|
579
586
|
const fetchSpy = mockFetchForApproval({ reviews: [APPROVED_BY_REVIEWER] });
|
|
580
587
|
|
|
581
588
|
try {
|