@desplega.ai/agent-swarm 2.4.1 → 2.4.2
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-zvzr538d.js → acp-adapter-dv7hmx4r.js} +1 -1
- package/dist/{actions-jhtqdw69.js → actions-51nc1gny.js} +16 -16
- package/dist/{amp-adapter-bd84rhs2.js → amp-adapter-abp5tdx8.js} +1 -1
- package/dist/{app-kreq1gwg.js → app-9h14qvr6.js} +3 -3
- package/dist/{assistant-3k22sbsq.js → assistant-wv1gz3mt.js} +17 -17
- package/dist/{boot-reembed-e53x6b9v.js → boot-reembed-6yn53ms9.js} +1 -1
- package/dist/{boot-reembed-f6z923ny.js → boot-reembed-7fcka4z5.js} +1 -1
- package/dist/{boot-scrub-sweep-0538vsb7.js → boot-scrub-sweep-pm1580ax.js} +2 -2
- package/dist/{boot-seeds-6wxqkfk2.js → boot-seeds-g5hfc1x8.js} +4 -4
- package/dist/{claude-adapter-gfts5044.js → claude-adapter-gf3kxhf0.js} +2 -2
- package/dist/{claude-sdk-session-56besmfz.js → claude-sdk-session-s2k1qd8y.js} +2 -2
- package/dist/{cli-tbhwrxpt.js → cli-02qm7src.js} +3 -3
- package/dist/{cli-3kh4kpqy.js → cli-0qvtffe9.js} +5 -5
- package/dist/{cli-p6argxh9.js → cli-10hhg0km.js} +6 -6
- package/dist/{cli-gf0kfg81.js → cli-239yjk27.js} +2 -2
- package/dist/{cli-kr2pmfqx.js → cli-5rk36sjf.js} +72 -59
- package/dist/{cli-nx462e4v.js → cli-7awf1fp8.js} +1 -1
- package/dist/{cli-jh0qcrga.js → cli-8sxqsbbq.js} +1 -1
- package/dist/{cli-axh29tyr.js → cli-8zwe9mpk.js} +5 -5
- package/dist/{cli-z1ew8vbq.js → cli-ahtpy9kf.js} +1 -1
- package/dist/{cli-3r448dqn.js → cli-bcj753sq.js} +3 -3
- package/dist/{cli-rqqsfz30.js → cli-d3qqdy4c.js} +82 -12
- package/dist/{cli-z071fb95.js → cli-efaqv1c7.js} +4 -4
- package/dist/{cli-s48yvzb8.js → cli-faypxz7q.js} +3 -3
- package/dist/{cli-m2qgh5sv.js → cli-ftyypbg5.js} +1 -1
- package/dist/{cli-6ncjvx55.js → cli-fyzjmseb.js} +1 -1
- package/dist/{cli-0bh477zn.js → cli-h38rjv87.js} +117 -9
- package/dist/{cli-3p8fqcnh.js → cli-h93c9vb3.js} +1 -1
- package/dist/{cli-rdq7tzwm.js → cli-hyfw2j1e.js} +8 -8
- package/dist/{cli-bn5kevnf.js → cli-n85dqg6d.js} +2 -2
- package/dist/{cli-fz9kewbh.js → cli-nmqdc2t3.js} +1 -1
- package/dist/{cli-vy0fay6k.js → cli-nn47d7e8.js} +10 -10
- package/dist/{cli-mfe7f7b5.js → cli-pkht5hap.js} +1 -1
- package/dist/{cli-xftrvxag.js → cli-r99kc9yq.js} +1 -1
- package/dist/{cli-p0rakbrk.js → cli-rdyw3mct.js} +1 -1
- package/dist/{cli-sjyetj21.js → cli-s6vr7r2d.js} +1 -1
- package/dist/{cli-k9z4zd4p.js → cli-saw9a6f6.js} +2 -2
- package/dist/{cli-ash6xxk7.js → cli-shvfedvm.js} +2 -2
- package/dist/{cli-syhq378v.js → cli-t30yj95q.js} +1 -1
- package/dist/{cli-kmqgmcnb.js → cli-y2x52gs2.js} +4 -4
- package/dist/{cli-471qk6s2.js → cli-y8n5k515.js} +14 -14
- package/dist/{cli-3tnpj2bh.js → cli-z4wd2ftv.js} +1 -1
- package/dist/cli.js +12 -12
- package/dist/{codex-adapter-p6rmm7vv.js → codex-adapter-ttygw382.js} +1 -1
- package/dist/{codex-session-runner-8vd5t9t0.js → codex-session-runner-60b58vv3.js} +1 -1
- package/dist/{commands-7ems2p13.js → commands-edmdwa4x.js} +2 -2
- package/dist/{controllers-connect-bg33ae53.js → controllers-connect-5fym8sw1.js} +16 -16
- package/dist/{db-ys2rbdft.js → db-2y0mgstc.js} +3 -3
- package/dist/{db-w505dwa8.js → db-nn0hcmx5.js} +5 -1
- package/dist/{dispatcher-0s0mckqr.js → dispatcher-0a9wwjcx.js} +15 -15
- package/dist/{external-channel-5b23fv8z.js → external-channel-zhvt95n3.js} +2 -2
- package/dist/{grok-adapter-q9fghe5q.js → grok-adapter-qnr1hp2y.js} +2 -2
- package/dist/{handlers-mqrym347.js → handlers-dkcarkd0.js} +18 -18
- package/dist/{heartbeat-tww6yx1w.js → heartbeat-gz97r7ac.js} +17 -17
- package/dist/{hook-dx0pkvar.js → hook-1mmdgjqs.js} +1 -1
- package/dist/{hook-pvmh9b6m.js → hook-x8rthca4.js} +1 -1
- package/dist/{http-k2ejbt05.js → http-r8f85xr9.js} +111 -65
- package/dist/{index-pqfwh9nn.js → index-5jecgp3n.js} +17 -17
- package/dist/{index-h2ek08d6.js → index-aex0eba9.js} +15 -15
- package/dist/{index-x5aktmp8.js → index-smxyk04s.js} +1 -1
- package/dist/{index-dgz61nff.js → index-vzvfqzfj.js} +15 -15
- package/dist/{index-2z710v4x.js → index-y7rbcrc1.js} +8 -8
- package/dist/{keepalive-nhvpw10z.js → keepalive-64c0f34c.js} +3 -3
- package/dist/{lead-jb6sf69z.js → lead-b1da0f97.js} +23 -23
- package/dist/{maintenance-agakc519.js → maintenance-mnxpar12.js} +1 -1
- package/dist/{oauth-refresh-sweep-4dcbhtn0.js → oauth-refresh-sweep-36hs2v4h.js} +2 -2
- package/dist/{onboard-4n2kwa4v.js → onboard-xes8de61.js} +2 -2
- package/dist/{otel-impl-tfa12080.js → otel-impl-fzc57kcq.js} +1 -1
- package/dist/{pricing-refresh-3cqx3f3x.js → pricing-refresh-1nek2w7b.js} +3 -3
- package/dist/{rbac-roles-y6zbfzve.js → rbac-roles-sqefb5hw.js} +2 -2
- package/dist/{rbac-roles-9fae5rnv.js → rbac-roles-ttyfaep2.js} +1 -1
- package/dist/{render-v2-emawjvc4.js → render-v2-vb527jwn.js} +3 -3
- package/dist/{resume-zhgts7h7.js → resume-jcdzg1bn.js} +5 -5
- package/dist/{session-access-cm823e06.js → session-access-zsp4wsp0.js} +4 -4
- package/dist/{setup-tkp99w1e.js → setup-1nb2zqd3.js} +1 -1
- package/dist/{work-objects-f722h263.js → work-objects-61w08fwr.js} +18 -18
- package/dist/{worker-p7f336rq.js → worker-c1e68hpa.js} +23 -23
- package/openapi.json +393 -1
- package/package.json +2 -2
- package/src/apps/catalog.generated.json +3 -2
- package/src/apps/definition.ts +152 -4
- package/src/apps/row-store.ts +19 -7
- package/src/apps/schema-migrate.ts +14 -6
- package/src/apps/script-types.ts +1 -0
- package/src/apps/sync.ts +20 -19
- package/src/be/db/tasks/read.ts +112 -0
- package/src/be/db.ts +2 -0
- package/src/be/swarm-config-guard.ts +15 -0
- package/src/http/apps.ts +15 -2
- package/src/http/status.ts +49 -43
- package/src/http/tasks.ts +47 -0
- package/src/providers/acp-targets.ts +1 -1
- package/src/providers/amp-adapter.ts +1 -1
- package/src/telemetry.ts +15 -3
- package/src/tests/app-row-tools.test.ts +25 -0
- package/src/tests/apps-json-column.test.ts +611 -0
- package/src/tests/apps-script-types.test.ts +2 -0
- package/src/tests/apps-sync-engine.test.ts +42 -0
- package/src/tests/db-task-read-contract.test.ts +4 -0
- package/src/tests/db-task-write-contract.test.ts +2 -0
- package/src/tests/docs-index-footprint.test.js +2 -2
- package/src/tests/runner-harness-telemetry-env.test.ts +6 -0
- package/src/tests/status.test.ts +158 -9
- package/src/tests/task-lineage.test.ts +180 -0
- package/src/tests/telemetry-init.test.ts +62 -5
- package/src/tests/user-token-rest-auth.test.ts +38 -0
- package/src/tools/app-rows.ts +5 -0
- package/templates/skills/apps/SKILL.md +3 -2
- package/templates/skills/apps/content.md +3 -2
|
@@ -111,6 +111,7 @@ value:ReservedAgentRoleError
|
|
|
111
111
|
value:SESSION_ACTIVITY_REFRESH_MIN_INTERVAL_MS
|
|
112
112
|
value:SKILL_FILE_LIMITS
|
|
113
113
|
value:SUPERSEDED_RESUME_REPAIR_WINDOW_MS
|
|
114
|
+
value:TASK_LINEAGE_LIMITS
|
|
114
115
|
value:UNATTRIBUTED_USER_ID
|
|
115
116
|
value:__resetSqliteVecExtensionPathCacheForTests
|
|
116
117
|
value:abandonSlackOutcomeDelivery
|
|
@@ -412,6 +413,7 @@ value:getSystemDefaultSkills
|
|
|
412
413
|
value:getTaskAttachments
|
|
413
414
|
value:getTaskById
|
|
414
415
|
value:getTaskByWorkflowRunStepId
|
|
416
|
+
value:getTaskLineage
|
|
415
417
|
value:getTaskStats
|
|
416
418
|
value:getTasksByAgentId
|
|
417
419
|
value:getTasksByStatus
|
|
@@ -612,6 +614,7 @@ type:ExistingTrackerContextWorkReason
|
|
|
612
614
|
type:PendingChatRelay
|
|
613
615
|
type:TaskFilters
|
|
614
616
|
value:SUPERSEDED_RESUME_REPAIR_WINDOW_MS
|
|
617
|
+
value:TASK_LINEAGE_LIMITS
|
|
615
618
|
value:findCompletedTaskInThread
|
|
616
619
|
value:findExistingLinearTrackerContextWork
|
|
617
620
|
value:findRecentCancelledTaskInThread
|
|
@@ -639,6 +642,7 @@ value:getSlackTasksMissingTree
|
|
|
639
642
|
value:getSupersededTasksWithUnsettledDependents
|
|
640
643
|
value:getSupersededTasksWithoutResume
|
|
641
644
|
value:getTaskById
|
|
645
|
+
value:getTaskLineage
|
|
642
646
|
value:getTaskStats
|
|
643
647
|
value:getTasksByAgentId
|
|
644
648
|
value:getTasksByStatus
|
|
@@ -134,6 +134,7 @@ value:ReservedAgentRoleError
|
|
|
134
134
|
value:SESSION_ACTIVITY_REFRESH_MIN_INTERVAL_MS
|
|
135
135
|
value:SKILL_FILE_LIMITS
|
|
136
136
|
value:SUPERSEDED_RESUME_REPAIR_WINDOW_MS
|
|
137
|
+
value:TASK_LINEAGE_LIMITS
|
|
137
138
|
value:UNATTRIBUTED_USER_ID
|
|
138
139
|
value:__resetSqliteVecExtensionPathCacheForTests
|
|
139
140
|
value:abandonSlackOutcomeDelivery
|
|
@@ -435,6 +436,7 @@ value:getSystemDefaultSkills
|
|
|
435
436
|
value:getTaskAttachments
|
|
436
437
|
value:getTaskById
|
|
437
438
|
value:getTaskByWorkflowRunStepId
|
|
439
|
+
value:getTaskLineage
|
|
438
440
|
value:getTaskStats
|
|
439
441
|
value:getTasksByAgentId
|
|
440
442
|
value:getTasksByStatus
|
|
@@ -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(437);
|
|
107
|
+
expect(generatedKeys).toHaveLength(437);
|
|
108
108
|
expect(new Set(generatedKeys)).toEqual(new Set(operationKeys));
|
|
109
109
|
});
|
|
110
110
|
|
|
@@ -11,6 +11,12 @@ const TELEMETRY_PATH = join(import.meta.dir, "..", "telemetry.ts");
|
|
|
11
11
|
// leaves the process), and print the environment the event carries.
|
|
12
12
|
const BOOT_SCRIPT = `
|
|
13
13
|
const t = await import(${JSON.stringify(TELEMETRY_PATH)});
|
|
14
|
+
const { mkdtempSync, rmSync } = await import("node:fs");
|
|
15
|
+
const { tmpdir } = await import("node:os");
|
|
16
|
+
const { pathToFileURL } = await import("node:url");
|
|
17
|
+
const root = mkdtempSync(tmpdir() + "/telemetry-package-");
|
|
18
|
+
t.getTelemetryEnvironment(pathToFileURL(root + "/"));
|
|
19
|
+
rmSync(root, { recursive: true, force: true });
|
|
14
20
|
let body;
|
|
15
21
|
globalThis.fetch = async (_url, init) => {
|
|
16
22
|
body = JSON.parse(init.body);
|
package/src/tests/status.test.ts
CHANGED
|
@@ -115,7 +115,6 @@ const ENV_KEYS_TO_RESET = [
|
|
|
115
115
|
"GOOGLE_APPLICATION_CREDENTIALS",
|
|
116
116
|
"AGENTMAIL_API_KEY",
|
|
117
117
|
"AGENTMAIL_DISABLE",
|
|
118
|
-
"SWARM_VERIFY_TTL_MS",
|
|
119
118
|
"ANONYMIZED_TELEMETRY",
|
|
120
119
|
];
|
|
121
120
|
|
|
@@ -314,7 +313,7 @@ describe("setup milestones", () => {
|
|
|
314
313
|
expect(getMilestone(payload, "harness").state).toBe("verified");
|
|
315
314
|
});
|
|
316
315
|
|
|
317
|
-
test("provider rollup counts every agent with a
|
|
316
|
+
test("provider rollup counts every agent with a passing report in verifiedWorkers", async () => {
|
|
318
317
|
const verified = await createAgent({
|
|
319
318
|
name: "w-verified",
|
|
320
319
|
isLead: false,
|
|
@@ -376,7 +375,7 @@ describe("setup milestones", () => {
|
|
|
376
375
|
|
|
377
376
|
// ─── Multi-provider fleet rollup ─────────────────────────────────────────
|
|
378
377
|
describe("harness — multi-provider fleet aggregate", () => {
|
|
379
|
-
test("`verified` when every provider in the fleet has a
|
|
378
|
+
test("`verified` when every provider in the fleet has a passing live test", async () => {
|
|
380
379
|
const a = await createAgent({
|
|
381
380
|
name: "claude-w",
|
|
382
381
|
isLead: false,
|
|
@@ -423,8 +422,30 @@ describe("setup milestones", () => {
|
|
|
423
422
|
|
|
424
423
|
const m = getMilestone(await buildStatusPayload(), "harness");
|
|
425
424
|
expect(m.state).toBe("configured");
|
|
426
|
-
|
|
427
|
-
expect(m.hint).
|
|
425
|
+
// Only the provider that needs attention is listed.
|
|
426
|
+
expect(m.hint).toBe("codex: not live-tested");
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
test("an all-verified fleet carries no hint, however old the passing tests are", async () => {
|
|
430
|
+
const a = await createAgent({
|
|
431
|
+
name: "claude-w",
|
|
432
|
+
isLead: false,
|
|
433
|
+
status: "idle",
|
|
434
|
+
capabilities: [],
|
|
435
|
+
});
|
|
436
|
+
const b = await createAgent({
|
|
437
|
+
name: "codex-w",
|
|
438
|
+
isLead: false,
|
|
439
|
+
status: "idle",
|
|
440
|
+
capabilities: [],
|
|
441
|
+
});
|
|
442
|
+
const old = { ok: true, error: null, latency_ms: 12, testedAt: Date.now() - 6 * 3_600_000 };
|
|
443
|
+
await seedCredStatus(a.id, "claude", { ready: true, satisfiedBy: "env", liveTest: old });
|
|
444
|
+
await seedCredStatus(b.id, "codex", { ready: true, satisfiedBy: "file", liveTest: old });
|
|
445
|
+
|
|
446
|
+
const m = getMilestone(await buildStatusPayload(), "harness");
|
|
447
|
+
expect(m.state).toBe("verified");
|
|
448
|
+
expect(m.hint).toBeUndefined();
|
|
428
449
|
});
|
|
429
450
|
|
|
430
451
|
test("`unverified` when any provider in the fleet reports blocked credentials", async () => {
|
|
@@ -1062,6 +1083,135 @@ describe("computeHealth (Phase 2)", () => {
|
|
|
1062
1083
|
expect(payload.health).toBe("degraded");
|
|
1063
1084
|
});
|
|
1064
1085
|
|
|
1086
|
+
test("`ok` when live workers' last passing live test is hours old", async () => {
|
|
1087
|
+
const lead = await createAgent({
|
|
1088
|
+
name: "lead-old",
|
|
1089
|
+
isLead: true,
|
|
1090
|
+
status: "idle",
|
|
1091
|
+
capabilities: [],
|
|
1092
|
+
});
|
|
1093
|
+
const worker = await createAgent({
|
|
1094
|
+
name: "worker-old",
|
|
1095
|
+
isLead: false,
|
|
1096
|
+
status: "idle",
|
|
1097
|
+
capabilities: [],
|
|
1098
|
+
});
|
|
1099
|
+
await updateAgentActivity(lead.id);
|
|
1100
|
+
await updateAgentActivity(worker.id);
|
|
1101
|
+
const oldPass = { ok: true, error: null, latency_ms: 12, testedAt: Date.now() - 6 * 3_600_000 };
|
|
1102
|
+
await seedCredStatus(lead.id, "claude", { ready: true, satisfiedBy: "env", liveTest: oldPass });
|
|
1103
|
+
await seedCredStatus(worker.id, "claude", {
|
|
1104
|
+
ready: true,
|
|
1105
|
+
satisfiedBy: "env",
|
|
1106
|
+
liveTest: oldPass,
|
|
1107
|
+
});
|
|
1108
|
+
|
|
1109
|
+
const payload = await buildStatusPayload();
|
|
1110
|
+
expect(getMilestone(payload, "workers").state).toBe("verified");
|
|
1111
|
+
expect(getMilestone(payload, "harness").state).toBe("verified");
|
|
1112
|
+
expect(payload.health).toBe("ok");
|
|
1113
|
+
});
|
|
1114
|
+
|
|
1115
|
+
test("`degraded` when the provider's latest live test failed, even after an older pass", async () => {
|
|
1116
|
+
const passed = await createAgent({
|
|
1117
|
+
name: "worker-pass",
|
|
1118
|
+
isLead: false,
|
|
1119
|
+
status: "idle",
|
|
1120
|
+
capabilities: [],
|
|
1121
|
+
});
|
|
1122
|
+
const failed = await createAgent({
|
|
1123
|
+
name: "worker-fail",
|
|
1124
|
+
isLead: false,
|
|
1125
|
+
status: "idle",
|
|
1126
|
+
capabilities: [],
|
|
1127
|
+
});
|
|
1128
|
+
await updateAgentActivity(passed.id);
|
|
1129
|
+
await updateAgentActivity(failed.id);
|
|
1130
|
+
await seedCredStatus(passed.id, "claude", {
|
|
1131
|
+
ready: true,
|
|
1132
|
+
satisfiedBy: "env",
|
|
1133
|
+
liveTest: { ok: true, error: null, latency_ms: 12, testedAt: Date.now() - 60_000 },
|
|
1134
|
+
});
|
|
1135
|
+
await seedCredStatus(failed.id, "claude", {
|
|
1136
|
+
ready: true,
|
|
1137
|
+
satisfiedBy: "env",
|
|
1138
|
+
liveTest: { ok: false, error: "HTTP 401", latency_ms: 12, testedAt: Date.now() },
|
|
1139
|
+
});
|
|
1140
|
+
|
|
1141
|
+
const payload = await buildStatusPayload();
|
|
1142
|
+
const harness = getMilestone(payload, "harness");
|
|
1143
|
+
expect(harness.state).toBe("configured");
|
|
1144
|
+
expect(harness.hint).toBe("claude: live test failed");
|
|
1145
|
+
expect(payload.health).toBe("degraded");
|
|
1146
|
+
});
|
|
1147
|
+
|
|
1148
|
+
test("an offline agent's newer failed live test does not override an online pass", async () => {
|
|
1149
|
+
const online = await createAgent({
|
|
1150
|
+
name: "worker-online",
|
|
1151
|
+
isLead: false,
|
|
1152
|
+
status: "idle",
|
|
1153
|
+
capabilities: [],
|
|
1154
|
+
});
|
|
1155
|
+
const offline = await createAgent({
|
|
1156
|
+
name: "worker-offline",
|
|
1157
|
+
isLead: false,
|
|
1158
|
+
status: "offline",
|
|
1159
|
+
capabilities: [],
|
|
1160
|
+
});
|
|
1161
|
+
await updateAgentActivity(online.id);
|
|
1162
|
+
await seedCredStatus(online.id, "claude", {
|
|
1163
|
+
ready: true,
|
|
1164
|
+
satisfiedBy: "env",
|
|
1165
|
+
liveTest: { ok: true, error: null, latency_ms: 12, testedAt: Date.now() - 6 * 3_600_000 },
|
|
1166
|
+
});
|
|
1167
|
+
await seedCredStatus(offline.id, "claude", {
|
|
1168
|
+
ready: true,
|
|
1169
|
+
satisfiedBy: "env",
|
|
1170
|
+
liveTest: { ok: false, error: "HTTP 401", latency_ms: 12, testedAt: Date.now() },
|
|
1171
|
+
});
|
|
1172
|
+
|
|
1173
|
+
expect(await rollupCredStatusForProvider("claude")).toMatchObject({
|
|
1174
|
+
state: "verified",
|
|
1175
|
+
workers: 1,
|
|
1176
|
+
reports: 1,
|
|
1177
|
+
});
|
|
1178
|
+
const harness = getMilestone(await buildStatusPayload(), "harness");
|
|
1179
|
+
expect(harness.state).toBe("verified");
|
|
1180
|
+
expect(harness.hint).toBeUndefined();
|
|
1181
|
+
});
|
|
1182
|
+
|
|
1183
|
+
test("a provider reported only by offline agents does not degrade the harness", async () => {
|
|
1184
|
+
const online = await createAgent({
|
|
1185
|
+
name: "claude-online",
|
|
1186
|
+
isLead: false,
|
|
1187
|
+
status: "idle",
|
|
1188
|
+
capabilities: [],
|
|
1189
|
+
});
|
|
1190
|
+
const offline = await createAgent({
|
|
1191
|
+
name: "codex-offline",
|
|
1192
|
+
isLead: false,
|
|
1193
|
+
status: "offline",
|
|
1194
|
+
capabilities: [],
|
|
1195
|
+
});
|
|
1196
|
+
await updateAgentActivity(online.id);
|
|
1197
|
+
await seedCredStatus(online.id, "claude", {
|
|
1198
|
+
ready: true,
|
|
1199
|
+
satisfiedBy: "env",
|
|
1200
|
+
liveTest: { ok: true, error: null, latency_ms: 12, testedAt: Date.now() },
|
|
1201
|
+
});
|
|
1202
|
+
await seedCredStatus(offline.id, "codex", {
|
|
1203
|
+
ready: false,
|
|
1204
|
+
missing: ["OPENAI_API_KEY"],
|
|
1205
|
+
satisfiedBy: null,
|
|
1206
|
+
liveTest: { ok: false, error: "HTTP 401", latency_ms: 12, testedAt: Date.now() },
|
|
1207
|
+
});
|
|
1208
|
+
|
|
1209
|
+
const harness = getMilestone(await buildStatusPayload(), "harness");
|
|
1210
|
+
expect(harness.state).toBe("verified");
|
|
1211
|
+
expect(harness.hint).toBeUndefined();
|
|
1212
|
+
expect((harness.providers ?? []).map((p) => p.provider)).toEqual(["claude"]);
|
|
1213
|
+
});
|
|
1214
|
+
|
|
1065
1215
|
test("`ok` when workers are `configured` (heartbeat drift is a runtime concern, not setup health)", async () => {
|
|
1066
1216
|
// Workers in `configured` state means agents exist but haven't posted a
|
|
1067
1217
|
// heartbeat in the last 5 minutes. This is surfaced on /agents and the
|
|
@@ -1166,9 +1316,8 @@ describe("worker-reported live test drives harness.state", () => {
|
|
|
1166
1316
|
expect(getMilestone(await buildStatusPayload(), "harness").state).toBe("verified");
|
|
1167
1317
|
});
|
|
1168
1318
|
|
|
1169
|
-
test("
|
|
1319
|
+
test("an old passing live test keeps harness `verified`", async () => {
|
|
1170
1320
|
process.env.HARNESS_PROVIDER = "claude";
|
|
1171
|
-
process.env.SWARM_VERIFY_TTL_MS = "1000"; // 1s — anything older is stale
|
|
1172
1321
|
const a = await createAgent({ name: "w-stl", isLead: false, status: "idle", capabilities: [] });
|
|
1173
1322
|
await seedCredStatus(a.id, "claude", {
|
|
1174
1323
|
ready: true,
|
|
@@ -1176,10 +1325,10 @@ describe("worker-reported live test drives harness.state", () => {
|
|
|
1176
1325
|
ok: true,
|
|
1177
1326
|
error: null,
|
|
1178
1327
|
latency_ms: 80,
|
|
1179
|
-
testedAt: Date.now() -
|
|
1328
|
+
testedAt: Date.now() - 24 * 3_600_000, // a day ago: workers only re-test on change
|
|
1180
1329
|
},
|
|
1181
1330
|
});
|
|
1182
|
-
expect(getMilestone(await buildStatusPayload(), "harness").state).toBe("
|
|
1331
|
+
expect(getMilestone(await buildStatusPayload(), "harness").state).toBe("verified");
|
|
1183
1332
|
});
|
|
1184
1333
|
|
|
1185
1334
|
test("a failed live test still leaves harness `configured` if presence is ready", async () => {
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
|
+
import { unlink } from "node:fs/promises";
|
|
3
|
+
import {
|
|
4
|
+
closeDb,
|
|
5
|
+
createAgent,
|
|
6
|
+
createTaskExtended,
|
|
7
|
+
getDbClient,
|
|
8
|
+
getTaskLineage,
|
|
9
|
+
initDb,
|
|
10
|
+
TASK_LINEAGE_LIMITS,
|
|
11
|
+
} from "../be/db";
|
|
12
|
+
|
|
13
|
+
const TEST_DB_PATH = "./test-task-lineage.sqlite";
|
|
14
|
+
|
|
15
|
+
async function removeDb() {
|
|
16
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
17
|
+
try {
|
|
18
|
+
await unlink(`${TEST_DB_PATH}${suffix}`);
|
|
19
|
+
} catch {}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe("getTaskLineage", () => {
|
|
24
|
+
let agentId: string;
|
|
25
|
+
|
|
26
|
+
beforeAll(async () => {
|
|
27
|
+
await removeDb();
|
|
28
|
+
initDb(TEST_DB_PATH);
|
|
29
|
+
const agent = await createAgent({
|
|
30
|
+
id: crypto.randomUUID(),
|
|
31
|
+
name: "Lineage Test Agent",
|
|
32
|
+
isLead: false,
|
|
33
|
+
status: "idle",
|
|
34
|
+
});
|
|
35
|
+
agentId = agent.id;
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
/** A straight parentTaskId chain, root first. */
|
|
39
|
+
async function createChain(label: string, length: number) {
|
|
40
|
+
const chain = [await createTaskExtended(`${label} 0`, { agentId })];
|
|
41
|
+
for (let i = 1; i < length; i++) {
|
|
42
|
+
chain.push(
|
|
43
|
+
await createTaskExtended(`${label} ${i}`, { agentId, parentTaskId: chain[i - 1]!.id }),
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
return chain;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
afterAll(async () => {
|
|
50
|
+
closeDb();
|
|
51
|
+
await removeDb();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("unknown task: null", async () => {
|
|
55
|
+
expect(await getTaskLineage("no-such-task")).toBeNull();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("from a grandchild: walks up to the root, then the whole tree", async () => {
|
|
59
|
+
const root = await createTaskExtended("root", { agentId });
|
|
60
|
+
const childA = await createTaskExtended("child a", { agentId, parentTaskId: root.id });
|
|
61
|
+
const childB = await createTaskExtended("child b", { agentId, parentTaskId: root.id });
|
|
62
|
+
const grandchild = await createTaskExtended("grandchild", {
|
|
63
|
+
agentId,
|
|
64
|
+
parentTaskId: childA.id,
|
|
65
|
+
});
|
|
66
|
+
const greatGrandchild = await createTaskExtended("great-grandchild", {
|
|
67
|
+
agentId,
|
|
68
|
+
parentTaskId: grandchild.id,
|
|
69
|
+
});
|
|
70
|
+
const unrelated = await createTaskExtended("unrelated", { agentId });
|
|
71
|
+
|
|
72
|
+
const lineage = await getTaskLineage(grandchild.id);
|
|
73
|
+
expect(lineage).not.toBeNull();
|
|
74
|
+
expect(lineage!.rootTaskId).toBe(root.id);
|
|
75
|
+
expect(lineage!.truncated).toBe(false);
|
|
76
|
+
expect(lineage!.tasks[0]!.id).toBe(root.id);
|
|
77
|
+
const ids = new Set(lineage!.tasks.map((t) => t.id));
|
|
78
|
+
expect(ids).toEqual(
|
|
79
|
+
new Set([root.id, childA.id, childB.id, grandchild.id, greatGrandchild.id]),
|
|
80
|
+
);
|
|
81
|
+
expect(ids.has(unrelated.id)).toBe(false);
|
|
82
|
+
expect(lineage!.tasks.find((t) => t.id === childA.id)!.parentTaskId).toBe(root.id);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("a node cap keeps the path to the task and its own subtree, and says it cut", async () => {
|
|
86
|
+
const root = await createTaskExtended("wide root", { agentId });
|
|
87
|
+
const children = [];
|
|
88
|
+
for (let i = 0; i < 6; i++) {
|
|
89
|
+
children.push(await createTaskExtended(`fan-out ${i}`, { agentId, parentTaskId: root.id }));
|
|
90
|
+
}
|
|
91
|
+
const last = children[children.length - 1]!;
|
|
92
|
+
const leaf = await createTaskExtended("leaf under the last child", {
|
|
93
|
+
agentId,
|
|
94
|
+
parentTaskId: last.id,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const lineage = await getTaskLineage(last.id, { maxNodes: 3 });
|
|
98
|
+
expect(lineage!.truncated).toBe(true);
|
|
99
|
+
const ids = new Set(lineage!.tasks.map((t) => t.id));
|
|
100
|
+
expect(ids.has(root.id)).toBe(true);
|
|
101
|
+
expect(ids.has(last.id)).toBe(true);
|
|
102
|
+
expect(ids.has(leaf.id)).toBe(true);
|
|
103
|
+
expect(lineage!.tasks.length).toBeLessThanOrEqual(2 * 3 + 2);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("a depth cap stops the walk down", async () => {
|
|
107
|
+
const root = await createTaskExtended("deep root", { agentId });
|
|
108
|
+
let parent = root;
|
|
109
|
+
for (let i = 0; i < 5; i++) {
|
|
110
|
+
parent = await createTaskExtended(`level ${i + 1}`, { agentId, parentTaskId: parent.id });
|
|
111
|
+
}
|
|
112
|
+
const lineage = await getTaskLineage(root.id, { maxDepth: 2 });
|
|
113
|
+
expect(lineage!.tasks).toHaveLength(3);
|
|
114
|
+
expect(lineage!.truncated).toBe(true);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("a chain past the default depth cap says it cut", async () => {
|
|
118
|
+
const chain = await createChain("default-depth", 32);
|
|
119
|
+
const lineage = await getTaskLineage(chain[0]!.id);
|
|
120
|
+
expect(lineage!.tasks).toHaveLength(TASK_LINEAGE_LIMITS.maxDepth + 1);
|
|
121
|
+
expect(lineage!.truncated).toBe(true);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("a chain that ends exactly at the depth cap is complete", async () => {
|
|
125
|
+
const chain = await createChain("exact-depth", 3);
|
|
126
|
+
const lineage = await getTaskLineage(chain[0]!.id, { maxDepth: 2 });
|
|
127
|
+
expect(lineage!.tasks).toHaveLength(3);
|
|
128
|
+
expect(lineage!.truncated).toBe(false);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("every ancestor of the task survives a node cap of 1", async () => {
|
|
132
|
+
const chain = await createChain("path-node-cap", 5);
|
|
133
|
+
const leaf = chain[chain.length - 1]!;
|
|
134
|
+
const lineage = await getTaskLineage(leaf.id, { maxNodes: 1 });
|
|
135
|
+
expect(new Set(lineage!.tasks.map((t) => t.id))).toEqual(new Set(chain.map((t) => t.id)));
|
|
136
|
+
// Nothing in the tree is left out, so the result is complete.
|
|
137
|
+
expect(lineage!.truncated).toBe(false);
|
|
138
|
+
const ids = new Set(lineage!.tasks.map((t) => t.id));
|
|
139
|
+
for (const task of lineage!.tasks) {
|
|
140
|
+
if (task.id !== lineage!.rootTaskId) expect(ids.has(task.parentTaskId!)).toBe(true);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("a deep focused path survives both caps, and a cut sibling still flags", async () => {
|
|
145
|
+
const chain = await createChain("deep-focus", 8);
|
|
146
|
+
const sibling = await createTaskExtended("off-path sibling", {
|
|
147
|
+
agentId,
|
|
148
|
+
parentTaskId: chain[1]!.id,
|
|
149
|
+
});
|
|
150
|
+
const leaf = chain[chain.length - 1]!;
|
|
151
|
+
const lineage = await getTaskLineage(leaf.id, { maxNodes: 1, maxDepth: 1 });
|
|
152
|
+
const ids = new Set(lineage!.tasks.map((t) => t.id));
|
|
153
|
+
for (const task of chain) expect(ids.has(task.id)).toBe(true);
|
|
154
|
+
expect(ids.has(sibling.id)).toBe(false);
|
|
155
|
+
expect(lineage!.truncated).toBe(true);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test("a fan-out exactly at the node cap is complete; one more child cuts", async () => {
|
|
159
|
+
const root = await createTaskExtended("cap root", { agentId });
|
|
160
|
+
for (let i = 0; i < 3; i++) {
|
|
161
|
+
await createTaskExtended(`cap child ${i}`, { agentId, parentTaskId: root.id });
|
|
162
|
+
}
|
|
163
|
+
const atCap = await getTaskLineage(root.id, { maxNodes: 4 });
|
|
164
|
+
expect(atCap!.tasks).toHaveLength(4);
|
|
165
|
+
expect(atCap!.truncated).toBe(false);
|
|
166
|
+
const overCap = await getTaskLineage(root.id, { maxNodes: 3 });
|
|
167
|
+
expect(overCap!.tasks).toHaveLength(3);
|
|
168
|
+
expect(overCap!.truncated).toBe(true);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("a parentTaskId cycle ends at the ancestor cap instead of looping", async () => {
|
|
172
|
+
const a = await createTaskExtended("cycle a", { agentId });
|
|
173
|
+
const b = await createTaskExtended("cycle b", { agentId, parentTaskId: a.id });
|
|
174
|
+
await getDbClient().run("UPDATE agent_tasks SET parentTaskId = ? WHERE id = ?", [b.id, a.id]);
|
|
175
|
+
|
|
176
|
+
const lineage = await getTaskLineage(a.id, { maxAncestors: 5, maxDepth: 4 });
|
|
177
|
+
expect(lineage!.truncated).toBe(true);
|
|
178
|
+
expect(new Set(lineage!.tasks.map((t) => t.id))).toEqual(new Set([a.id, b.id]));
|
|
179
|
+
});
|
|
180
|
+
});
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdirSync, mkdtempSync, rmSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { pathToFileURL } from "node:url";
|
|
2
6
|
import pkg from "../../package.json";
|
|
3
7
|
import {
|
|
4
8
|
_getInstallationIdForTests,
|
|
@@ -14,6 +18,7 @@ import {
|
|
|
14
18
|
_resolveIntegrationProvider,
|
|
15
19
|
_resolveIntegrationType,
|
|
16
20
|
emitIntegrationConnected,
|
|
21
|
+
getTelemetryEnvironment,
|
|
17
22
|
initTelemetry,
|
|
18
23
|
telemetry,
|
|
19
24
|
track,
|
|
@@ -686,8 +691,11 @@ describe("initTelemetry", () => {
|
|
|
686
691
|
const originalFetch = globalThis.fetch;
|
|
687
692
|
const originalNodeEnv = process.env.NODE_ENV;
|
|
688
693
|
let captured: Record<string, unknown> | null = null;
|
|
694
|
+
let packageRoot: URL;
|
|
689
695
|
|
|
690
696
|
beforeEach(() => {
|
|
697
|
+
packageRoot = pathToFileURL(`${mkdtempSync(join(tmpdir(), "telemetry-env-"))}/`);
|
|
698
|
+
process.env.NODE_ENV = "production";
|
|
691
699
|
captured = null;
|
|
692
700
|
globalThis.fetch = (async (_url: string, init?: { body?: string }) => {
|
|
693
701
|
captured = init?.body ? JSON.parse(init.body) : null;
|
|
@@ -697,14 +705,59 @@ describe("initTelemetry", () => {
|
|
|
697
705
|
});
|
|
698
706
|
|
|
699
707
|
afterEach(() => {
|
|
708
|
+
rmSync(packageRoot, { recursive: true, force: true });
|
|
700
709
|
globalThis.fetch = originalFetch;
|
|
701
710
|
delete process.env.DESPLEGA_TELEMETRY_ENV;
|
|
702
711
|
if (originalNodeEnv === undefined) delete process.env.NODE_ENV;
|
|
703
712
|
else process.env.NODE_ENV = originalNodeEnv;
|
|
704
713
|
});
|
|
705
714
|
|
|
715
|
+
test("track detects the real checkout through the default package root with an unprimed cache", async () => {
|
|
716
|
+
await initTelemetry(
|
|
717
|
+
"api-server",
|
|
718
|
+
async (key) => (key === "telemetry_installation_id" ? "install_checkout_env" : undefined),
|
|
719
|
+
async () => {},
|
|
720
|
+
{ generateIfMissing: true },
|
|
721
|
+
);
|
|
722
|
+
|
|
723
|
+
track({ event: "test.event", properties: {} });
|
|
724
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
725
|
+
|
|
726
|
+
const metadata = (captured as { metadata: Record<string, unknown> }).metadata;
|
|
727
|
+
expect(metadata.environment).toBe("development");
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
test("a .git directory marks a checkout as development", () => {
|
|
731
|
+
mkdirSync(new URL(".git", packageRoot));
|
|
732
|
+
expect(getTelemetryEnvironment(packageRoot)).toBe("development");
|
|
733
|
+
rmSync(new URL(".git", packageRoot), { recursive: true });
|
|
734
|
+
expect(getTelemetryEnvironment(packageRoot)).toBe("development");
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
test("a .git file marks a worktree as development", async () => {
|
|
738
|
+
await Bun.write(new URL(".git", packageRoot), "gitdir: /some/worktree");
|
|
739
|
+
expect(getTelemetryEnvironment(packageRoot)).toBe("development");
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
test("caches the checkout check across events", () => {
|
|
743
|
+
expect(getTelemetryEnvironment(packageRoot)).toBe("production");
|
|
744
|
+
mkdirSync(new URL(".git", packageRoot));
|
|
745
|
+
expect(getTelemetryEnvironment(packageRoot)).toBe("production");
|
|
746
|
+
process.env.DESPLEGA_TELEMETRY_ENV = "staging";
|
|
747
|
+
expect(getTelemetryEnvironment(packageRoot)).toBe("staging");
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
test("an unusable package root falls back to production without throwing", () => {
|
|
751
|
+
expect(getTelemetryEnvironment(new URL("data:text/plain,unavailable"))).toBe("production");
|
|
752
|
+
});
|
|
753
|
+
|
|
754
|
+
test("compiled binary paths default to production without throwing", () => {
|
|
755
|
+
expect(getTelemetryEnvironment(new URL("file:///$bunfs/root/"))).toBe("production");
|
|
756
|
+
});
|
|
757
|
+
|
|
706
758
|
test("defaults to production even when NODE_ENV is development", async () => {
|
|
707
759
|
process.env.NODE_ENV = "development";
|
|
760
|
+
expect(getTelemetryEnvironment(packageRoot)).toBe("production");
|
|
708
761
|
await initTelemetry(
|
|
709
762
|
"api-server",
|
|
710
763
|
async (key) => (key === "telemetry_installation_id" ? "install_default_env" : undefined),
|
|
@@ -719,9 +772,11 @@ describe("initTelemetry", () => {
|
|
|
719
772
|
expect(metadata.environment).toBe("production");
|
|
720
773
|
});
|
|
721
774
|
|
|
722
|
-
test("uses DESPLEGA_TELEMETRY_ENV
|
|
723
|
-
|
|
724
|
-
process.env.
|
|
775
|
+
test("uses DESPLEGA_TELEMETRY_ENV over a git checkout and NODE_ENV=test", async () => {
|
|
776
|
+
mkdirSync(new URL(".git", packageRoot));
|
|
777
|
+
process.env.NODE_ENV = "test";
|
|
778
|
+
process.env.DESPLEGA_TELEMETRY_ENV = "staging";
|
|
779
|
+
expect(getTelemetryEnvironment(packageRoot)).toBe("staging");
|
|
725
780
|
await initTelemetry(
|
|
726
781
|
"api-server",
|
|
727
782
|
async (key) => (key === "telemetry_installation_id" ? "install_explicit_env" : undefined),
|
|
@@ -733,11 +788,13 @@ describe("initTelemetry", () => {
|
|
|
733
788
|
await new Promise((r) => setTimeout(r, 0));
|
|
734
789
|
|
|
735
790
|
const metadata = (captured as { metadata: Record<string, unknown> }).metadata;
|
|
736
|
-
expect(metadata.environment).toBe("
|
|
791
|
+
expect(metadata.environment).toBe("staging");
|
|
737
792
|
});
|
|
738
793
|
|
|
739
|
-
test("preserves NODE_ENV=test when telemetry env is unset", async () => {
|
|
794
|
+
test("preserves NODE_ENV=test over a git checkout when telemetry env is unset", async () => {
|
|
795
|
+
mkdirSync(new URL(".git", packageRoot));
|
|
740
796
|
process.env.NODE_ENV = "test";
|
|
797
|
+
expect(getTelemetryEnvironment(packageRoot)).toBe("test");
|
|
741
798
|
await initTelemetry(
|
|
742
799
|
"api-server",
|
|
743
800
|
async (key) => (key === "telemetry_installation_id" ? "install_test_env" : undefined),
|
|
@@ -110,6 +110,44 @@ describe("normal REST API user-bound token auth", () => {
|
|
|
110
110
|
expect(body.requestedByUserId).toBeUndefined();
|
|
111
111
|
});
|
|
112
112
|
|
|
113
|
+
for (const caller of ["user", "operator", "agent"] as const) {
|
|
114
|
+
for (const explicit of [false, true]) {
|
|
115
|
+
test(`REST ${caller} follow-up ${explicit ? "keeps an explicit schema" : caller === "agent" ? "inherits the parent schema" : "does not inherit the parent schema"}`, async () => {
|
|
116
|
+
const user = await createUser({ name: "Schema follow-up user" });
|
|
117
|
+
const agent = await createAgent({ name: "Schema worker", status: "idle" });
|
|
118
|
+
const parentSchema = { type: "object", properties: { options: { type: "array" } } };
|
|
119
|
+
const explicitSchema = { type: "object", properties: { answer: { type: "string" } } };
|
|
120
|
+
const parent = await createTaskExtended("schema-carrying worker task", {
|
|
121
|
+
agentId: agent.id,
|
|
122
|
+
requestedByUserId: user.id,
|
|
123
|
+
outputSchema: parentSchema,
|
|
124
|
+
});
|
|
125
|
+
const token =
|
|
126
|
+
caller === "user" ? (await mintToken(user.id, "rest", ACTOR)).plaintext : API_KEY;
|
|
127
|
+
const res = await fetch(`http://localhost:${port}/api/tasks`, {
|
|
128
|
+
method: "POST",
|
|
129
|
+
headers: {
|
|
130
|
+
Authorization: `Bearer ${token}`,
|
|
131
|
+
"Content-Type": "application/json",
|
|
132
|
+
...(caller === "agent" ? { "X-Agent-ID": agent.id } : {}),
|
|
133
|
+
},
|
|
134
|
+
body: JSON.stringify({
|
|
135
|
+
task: "Continue the session",
|
|
136
|
+
parentTaskId: parent.id,
|
|
137
|
+
source: "ui",
|
|
138
|
+
...(explicit ? { outputSchema: explicitSchema } : {}),
|
|
139
|
+
}),
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
expect(res.status).toBe(201);
|
|
143
|
+
const body = (await res.json()) as { id: string; outputSchema?: unknown };
|
|
144
|
+
expect(body.outputSchema).toEqual(
|
|
145
|
+
explicit ? explicitSchema : caller === "agent" ? parentSchema : undefined,
|
|
146
|
+
);
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
113
151
|
test("global API key caller cannot spoof requestedByUserId via body — falls back to owned task context", async () => {
|
|
114
152
|
const legitRequester = await createUser({ name: "Legit Requester" });
|
|
115
153
|
const attacker = await createUser({ name: "Attacker" });
|
package/src/tools/app-rows.ts
CHANGED
|
@@ -53,6 +53,11 @@ function rowFilters(model: ModelDef, equals: Record<string, string | number | bo
|
|
|
53
53
|
{ path: `equals.${column}`, message: "Unknown or hidden column." },
|
|
54
54
|
]);
|
|
55
55
|
}
|
|
56
|
+
if (declared?.kind === "json") {
|
|
57
|
+
throw new AppRowValidationError([
|
|
58
|
+
{ path: `equals.${column}`, message: "json columns cannot be filtered" },
|
|
59
|
+
]);
|
|
60
|
+
}
|
|
56
61
|
if (!validValue(declared ?? { kind: SYSTEM_COLUMN_KINDS[column]! }, value)) {
|
|
57
62
|
throw new AppRowValidationError([
|
|
58
63
|
{ path: `equals.${column}`, message: "Filter value must match the column kind." },
|