@bongos/core 1.19.887 → 1.19.888
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/.bongos-core.json +23 -13
- package/docs/module-api-changelog.md +2 -0
- package/modules/agents/lib/validate.js +49 -2
- package/modules/agents/migrations/agents_005_cron_trigger.sql +78 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/module-api.js +1 -1
- package/tests/agents_cron_trigger.mjs +133 -0
- package/tests/agents_validate.mjs +13 -5
package/.bongos-core.json
CHANGED
|
@@ -2,22 +2,22 @@
|
|
|
2
2
|
"artifact": "bongos-core",
|
|
3
3
|
"manifest_schema": 1,
|
|
4
4
|
"generator": "scripts/gds/package-core.js",
|
|
5
|
-
"core_version": "1.19.
|
|
6
|
-
"core_contract": "1.19.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.19.888",
|
|
6
|
+
"core_contract": "1.19.888",
|
|
7
|
+
"source_commit": "7ccf0b2f4cc7f7bafb93762cbd5cfb9ed37cf546",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-19T20:
|
|
9
|
+
"built_at": "2026-09-19T20:57:53.725Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 497,
|
|
13
13
|
"agent_docs_stubbed": 26,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 2266,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 2789,
|
|
20
|
+
"tree_sha256": "42c331016e53b14ca5416a325790e2846ea0ee2c6a24177222b823a1d2977b9c",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/ask-for-help/SKILL.md",
|
|
@@ -2607,7 +2607,7 @@
|
|
|
2607
2607
|
{
|
|
2608
2608
|
"path": "docs/module-api-changelog.md",
|
|
2609
2609
|
"mode": "0000644",
|
|
2610
|
-
"sha256": "
|
|
2610
|
+
"sha256": "7f60dfa2c479fecbd5a81de040689b9ff6f9d2aa5a907f9a7fcf86b77b476fea"
|
|
2611
2611
|
},
|
|
2612
2612
|
{
|
|
2613
2613
|
"path": "docs/modules-contract.md",
|
|
@@ -3732,7 +3732,7 @@
|
|
|
3732
3732
|
{
|
|
3733
3733
|
"path": "modules/agents/lib/validate.js",
|
|
3734
3734
|
"mode": "0000644",
|
|
3735
|
-
"sha256": "
|
|
3735
|
+
"sha256": "e9b3c905539a323d74a55f98455204bd46daca2daf49c476324e85684ba7971f"
|
|
3736
3736
|
},
|
|
3737
3737
|
{
|
|
3738
3738
|
"path": "modules/agents/migrations/agents_001_definitions.sql",
|
|
@@ -3754,6 +3754,11 @@
|
|
|
3754
3754
|
"mode": "0000644",
|
|
3755
3755
|
"sha256": "31a5ac35536cecd264bc989907f4cf094e050db0b8de85e7e3e7831cf7c25a9d"
|
|
3756
3756
|
},
|
|
3757
|
+
{
|
|
3758
|
+
"path": "modules/agents/migrations/agents_005_cron_trigger.sql",
|
|
3759
|
+
"mode": "0000644",
|
|
3760
|
+
"sha256": "5d422998b1d53ee015f4a494ccede1adcb1eb90bd501bd28c1d88f9840b87367"
|
|
3761
|
+
},
|
|
3757
3762
|
{
|
|
3758
3763
|
"path": "modules/agents/module.json",
|
|
3759
3764
|
"mode": "0000644",
|
|
@@ -7692,12 +7697,12 @@
|
|
|
7692
7697
|
{
|
|
7693
7698
|
"path": "package-lock.json",
|
|
7694
7699
|
"mode": "0000644",
|
|
7695
|
-
"sha256": "
|
|
7700
|
+
"sha256": "d3b1b9f84e1713ff7815c19a9bd02b6c4de4dd3b44a5ede68ff56fbd5ad0cfcd"
|
|
7696
7701
|
},
|
|
7697
7702
|
{
|
|
7698
7703
|
"path": "package.json",
|
|
7699
7704
|
"mode": "0000644",
|
|
7700
|
-
"sha256": "
|
|
7705
|
+
"sha256": "4cd9682e13a9a2e1e358c7bd500286378e7b5b8ceb1bb284a925b91ab8de2a08"
|
|
7701
7706
|
},
|
|
7702
7707
|
{
|
|
7703
7708
|
"path": "public-docs/index.html",
|
|
@@ -9702,7 +9707,7 @@
|
|
|
9702
9707
|
{
|
|
9703
9708
|
"path": "src/module-api.js",
|
|
9704
9709
|
"mode": "0000644",
|
|
9705
|
-
"sha256": "
|
|
9710
|
+
"sha256": "ae3867dc81f944026ce67bae25ca927232b84accc4ed03d4ef45798526c72c46"
|
|
9706
9711
|
},
|
|
9707
9712
|
{
|
|
9708
9713
|
"path": "src/module-loader/catalog.js",
|
|
@@ -9844,6 +9849,11 @@
|
|
|
9844
9849
|
"mode": "0000644",
|
|
9845
9850
|
"sha256": "eea8f9e0bdf2c84fabc81e16c30e4026a7a336c1e705d614e68b805957b9b74a"
|
|
9846
9851
|
},
|
|
9852
|
+
{
|
|
9853
|
+
"path": "tests/agents_cron_trigger.mjs",
|
|
9854
|
+
"mode": "0000644",
|
|
9855
|
+
"sha256": "be78fa37657214c39a4880d2e1b44a1647c8b509d63262ca9a4a334431d7bdde"
|
|
9856
|
+
},
|
|
9847
9857
|
{
|
|
9848
9858
|
"path": "tests/agents_event_router.mjs",
|
|
9849
9859
|
"mode": "0000644",
|
|
@@ -9877,7 +9887,7 @@
|
|
|
9877
9887
|
{
|
|
9878
9888
|
"path": "tests/agents_validate.mjs",
|
|
9879
9889
|
"mode": "0000644",
|
|
9880
|
-
"sha256": "
|
|
9890
|
+
"sha256": "164059d69fabbd5dd70a29b29c524b5359f0346b71091e73a5e4fafa5dbc0804"
|
|
9881
9891
|
},
|
|
9882
9892
|
{
|
|
9883
9893
|
"path": "tests/agents_write_routes.mjs",
|
|
@@ -2248,5 +2248,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
2248
2248
|
landed since 1.19.885 with no explicit bump. run 35467975408. (task 1002620)
|
|
2249
2249
|
1.19.887 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2250
2250
|
landed since 1.19.886 with no explicit bump. run 35468511941. (task 1002620)
|
|
2251
|
+
1.19.888 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2252
|
+
landed since 1.19.887 with no explicit bump. run 35468948342. (task 1002620)
|
|
2251
2253
|
---------------------------------------------------------------------------
|
|
2252
2254
|
```
|
|
@@ -48,8 +48,31 @@
|
|
|
48
48
|
const EVENT_CATALOG = Object.freeze(['task.shipped', 'idea.graded.full']);
|
|
49
49
|
|
|
50
50
|
// Mirrors agents_definitions_trigger_type_chk. 'event' consumes the catalog above;
|
|
51
|
-
// 'on-demand' is the historian shape (a builder asks, the agent answers)
|
|
52
|
-
|
|
51
|
+
// 'on-demand' is the historian shape (a builder asks, the agent answers); 'cron'
|
|
52
|
+
// fires on a cadence and consumes CADENCES below (task 1004008, agents_005).
|
|
53
|
+
const TRIGGER_TYPES = Object.freeze(['event', 'on-demand', 'cron']);
|
|
54
|
+
|
|
55
|
+
// The cadence vocabulary a 'cron' trigger may declare, in trigger_spec.cadence.
|
|
56
|
+
//
|
|
57
|
+
// DUPLICATED FROM scripts/gds/routine-schedule.js ON PURPOSE, for the reason
|
|
58
|
+
// RANK_ORDER below is duplicated: a module may require only src/module-api.js
|
|
59
|
+
// (ADR 0083), so it cannot reach into scripts/. The two copies are kept honest by
|
|
60
|
+
// tests/agents_cron_trigger.mjs, which reads routine-schedule.js's own CADENCES +
|
|
61
|
+
// isUniformDivisor and asserts this file agrees — a drifting copy fails there
|
|
62
|
+
// rather than at some future tick.
|
|
63
|
+
//
|
|
64
|
+
// N MUST DIVIDE 60 for the sub-hourly form. A cron step restarts each hour, so
|
|
65
|
+
// only a divisor yields a uniform period: `*/15` fires :00 :15 :30 :45 always,
|
|
66
|
+
// but `*/7` fires :00…:56 then waits 4 — a "7min" cadence that is not one.
|
|
67
|
+
const CADENCES = Object.freeze(['hourly', 'daily', 'weekly', 'monthly']);
|
|
68
|
+
const SUBHOURLY_RE = /^([1-9]\d*)min$/;
|
|
69
|
+
function isCadence(name) {
|
|
70
|
+
if (CADENCES.includes(name)) return true;
|
|
71
|
+
const m = SUBHOURLY_RE.exec(String(name == null ? '' : name));
|
|
72
|
+
if (!m) return false;
|
|
73
|
+
const n = Number(m[1]);
|
|
74
|
+
return Number.isInteger(n) && n > 0 && n < 60 && 60 % n === 0;
|
|
75
|
+
}
|
|
53
76
|
|
|
54
77
|
// Mirrors agents_definitions_model_tier_chk. No 'main' tier by design — an agent
|
|
55
78
|
// is a subagent, never the main session.
|
|
@@ -222,6 +245,29 @@ function validateAgentDefinition(obj, opts = {}) {
|
|
|
222
245
|
}
|
|
223
246
|
}
|
|
224
247
|
|
|
248
|
+
// --- the cadence, fail-closed, the same shape as the event catalog above ----
|
|
249
|
+
// agents_005's CHECK pins that a cron definition HAS a non-empty cadence; the
|
|
250
|
+
// vocabulary is pinned here, because the sub-hourly rule is a predicate and a
|
|
251
|
+
// CHECK spelling it out would be a second copy free to drift.
|
|
252
|
+
//
|
|
253
|
+
// The reach is deliberately the EVENT BLOCK'S reach, no more: an UNWIRED
|
|
254
|
+
// cadence is refused wherever it appears, including on a non-cron definition
|
|
255
|
+
// where it is dead config. A VALID cadence on a non-cron definition is ACCEPTED,
|
|
256
|
+
// exactly as a wired `event:` on an on-demand definition is — harmless, unread,
|
|
257
|
+
// and not this validator's call to police. Saying otherwise in a comment would
|
|
258
|
+
// describe a guard that is not here.
|
|
259
|
+
if (spec) {
|
|
260
|
+
const cadence = safeGet(spec, 'cadence');
|
|
261
|
+
const hasCadenceKey = cadence !== undefined && cadence !== null;
|
|
262
|
+
if (triggerType === 'cron' && !isNonEmptyString(cadence)) {
|
|
263
|
+
E('trigger_cadence_missing', 'trigger_spec.cadence',
|
|
264
|
+
`trigger_spec.cadence: required for a 'cron' trigger — a definition with no cadence can never fire. Named cadences: ${CADENCES.join(', ')}; or a sub-hourly '<N>min' where N divides 60.`);
|
|
265
|
+
} else if (hasCadenceKey && !isCadence(cadence)) {
|
|
266
|
+
E('trigger_cadence_unwired', 'trigger_spec.cadence',
|
|
267
|
+
`trigger_spec.cadence: ${JSON.stringify(cadence)} is not a cadence. Named cadences: ${CADENCES.join(', ')}; or a sub-hourly '<N>min' where N divides 60 (so the declared period and the realized one are the same claim).`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
225
271
|
// --- model tier -----------------------------------------------------------
|
|
226
272
|
const rawTier = safeGet(obj, 'model_tier');
|
|
227
273
|
const modelTier = rawTier === undefined || rawTier === null ? 'default' : rawTier;
|
|
@@ -355,6 +401,7 @@ function validateAgentDefinition(obj, opts = {}) {
|
|
|
355
401
|
module.exports = {
|
|
356
402
|
validateAgentDefinition,
|
|
357
403
|
EVENT_CATALOG,
|
|
404
|
+
CADENCES, isCadence,
|
|
358
405
|
TRIGGER_TYPES,
|
|
359
406
|
MODEL_TIERS,
|
|
360
407
|
RANK_ORDER,
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
-- agents_005_cron_trigger.sql — make 'cron' a trigger type that can exist (task 1004008).
|
|
2
|
+
--
|
|
3
|
+
-- Tasks 1002498 (the cron-router poller) and 1002501 (the cron roster) both ask
|
|
4
|
+
-- for a timer whose each tick SELECTS DUE CRON AGENTS. There are none and there
|
|
5
|
+
-- can be none: agents_001 pinned trigger_type to ('event','on-demand') on BOTH
|
|
6
|
+
-- agents_definitions and agents_runs, and lib/validate.js mirrors it. Build the
|
|
7
|
+
-- poller against that and you get a timer that is correct, tested, and selects
|
|
8
|
+
-- from an empty set by construction — green tests over an unreachable branch,
|
|
9
|
+
-- which is a false claim of coverage rather than coverage. This migration is the
|
|
10
|
+
-- enabling half; the poller is 1002498's and is deliberately NOT built here.
|
|
11
|
+
--
|
|
12
|
+
-- WIDENING A FAIL-CLOSED ENUM IS THE POINT, NOT A WORKAROUND. agents_001's header
|
|
13
|
+
-- calls these enums fail-closed on purpose: "an unwired trigger name is refused,
|
|
14
|
+
-- never silently stored". That rule is kept — 'cron' stops being unwired in the
|
|
15
|
+
-- same change that makes it storable, because lib/validate.js is widened and
|
|
16
|
+
-- constrained alongside. What is still true after this: a trigger name outside
|
|
17
|
+
-- the three is refused at both the validator and the constraint.
|
|
18
|
+
--
|
|
19
|
+
-- BOTH TABLES, AND WHY THE LEDGER MATTERS AS MUCH AS THE DEFINITION. agents_001
|
|
20
|
+
-- constrains trigger_type on agents_runs too. Widen only the definition and a
|
|
21
|
+
-- cron agent could be stored and enabled, and then fail at the moment it fires,
|
|
22
|
+
-- when the ledger insert violates its own CHECK — the worst shape available: a
|
|
23
|
+
-- definition that looks armed and cannot record a single run.
|
|
24
|
+
--
|
|
25
|
+
-- A CRON DEFINITION MUST SAY WHEN. agents_001 already refuses an event definition
|
|
26
|
+
-- with no event name ("a definition that can never fire"). A cron definition with
|
|
27
|
+
-- no cadence is the same defect wearing a different key, so it gets the same
|
|
28
|
+
-- treatment rather than being left for the poller to interpret at tick time.
|
|
29
|
+
-- The cadence VOCABULARY is validated in lib/validate.js, not here: the named set
|
|
30
|
+
-- ('hourly','daily','weekly','monthly') plus the open-ended sub-hourly form
|
|
31
|
+
-- ('15min') is a predicate, and a CHECK that tried to spell the sub-hourly rule —
|
|
32
|
+
-- N must divide 60 — would be a second, drifting copy of it. The constraint
|
|
33
|
+
-- pins the SHAPE (a non-empty cadence key is present); the validator pins the
|
|
34
|
+
-- VALUE. That is the same division agents_001 draws for `event`.
|
|
35
|
+
--
|
|
36
|
+
-- Module migrations are ADDITIVE and NAMESPACED (ADR 0083 §Decision #5): every
|
|
37
|
+
-- object is agents_-prefixed, there is no down-migration, and the filename is
|
|
38
|
+
-- agents_-prefixed (ADR 0108 §5). Idempotent: safe to re-run. No existing row
|
|
39
|
+
-- changes — widening a CHECK can only make rows MORE legal, so nothing that
|
|
40
|
+
-- validates today stops validating.
|
|
41
|
+
|
|
42
|
+
BEGIN;
|
|
43
|
+
|
|
44
|
+
DO $$
|
|
45
|
+
BEGIN
|
|
46
|
+
-- agents_definitions: the three WIRED trigger types.
|
|
47
|
+
IF EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'agents_definitions_trigger_type_chk') THEN
|
|
48
|
+
ALTER TABLE agents_definitions DROP CONSTRAINT agents_definitions_trigger_type_chk;
|
|
49
|
+
END IF;
|
|
50
|
+
ALTER TABLE agents_definitions ADD CONSTRAINT agents_definitions_trigger_type_chk
|
|
51
|
+
CHECK (trigger_type IN ('event', 'on-demand', 'cron'));
|
|
52
|
+
|
|
53
|
+
-- agents_runs: the same three, so a cron fire can be RECORDED and not only run.
|
|
54
|
+
IF EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'agents_runs_trigger_type_chk') THEN
|
|
55
|
+
ALTER TABLE agents_runs DROP CONSTRAINT agents_runs_trigger_type_chk;
|
|
56
|
+
END IF;
|
|
57
|
+
ALTER TABLE agents_runs ADD CONSTRAINT agents_runs_trigger_type_chk
|
|
58
|
+
CHECK (trigger_type IN ('event', 'on-demand', 'cron'));
|
|
59
|
+
|
|
60
|
+
-- The twin of agents_definitions_trigger_spec_event_chk: a cron definition
|
|
61
|
+
-- carries a non-empty cadence, or it is a definition that can never fire.
|
|
62
|
+
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'agents_definitions_trigger_spec_cadence_chk') THEN
|
|
63
|
+
ALTER TABLE agents_definitions ADD CONSTRAINT agents_definitions_trigger_spec_cadence_chk
|
|
64
|
+
CHECK (trigger_type <> 'cron'
|
|
65
|
+
OR (trigger_spec ? 'cadence' AND length(trigger_spec ->> 'cadence') > 0));
|
|
66
|
+
END IF;
|
|
67
|
+
END $$;
|
|
68
|
+
|
|
69
|
+
-- agents_001 indexes (trigger_type, trigger_spec->>'event') for the event
|
|
70
|
+
-- router's "who wants this event" read. The cron router's read is a different
|
|
71
|
+
-- question — "which cron agents are due" — and it is answered by scanning the
|
|
72
|
+
-- cron rows, which are few. Adding a cadence index now would be indexing for a
|
|
73
|
+
-- query no committed code makes; 1002498 adds one if its poller needs it.
|
|
74
|
+
|
|
75
|
+
COMMENT ON CONSTRAINT agents_definitions_trigger_type_chk ON agents_definitions IS
|
|
76
|
+
'The WIRED trigger vocabulary (task 1004008 added cron). Mirrored by TRIGGER_TYPES in modules/agents/lib/validate.js — widen both together or the two disagree about what can exist.';
|
|
77
|
+
|
|
78
|
+
COMMIT;
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.888",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.19.
|
|
9
|
+
"version": "1.19.888",
|
|
10
10
|
"license": "AGPL-3.0-or-later",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"express": "^4.21.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.888",
|
|
4
4
|
"description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"main": "src/platform-server.js",
|
package/src/module-api.js
CHANGED
|
@@ -71,7 +71,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
71
71
|
// there. scripts/gds/bump-version.js still rewrites the literal below; it appends
|
|
72
72
|
// the entry to that file. Look for a version's history there, not here.
|
|
73
73
|
// ---------------------------------------------------------------------------
|
|
74
|
-
const CORE_VERSION = '1.19.
|
|
74
|
+
const CORE_VERSION = '1.19.888'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
75
75
|
|
|
76
76
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
77
77
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// tests/agents_cron_trigger.mjs — 'cron' is a trigger type that can EXIST
|
|
2
|
+
// (task 1004008, goal 1000095).
|
|
3
|
+
//
|
|
4
|
+
// Tasks 1002498 (cron-router poller) and 1002501 (cron roster) both select "due
|
|
5
|
+
// cron agents" from a set that was empty by construction: agents_001 pinned
|
|
6
|
+
// trigger_type to ('event','on-demand') on BOTH agents_definitions and
|
|
7
|
+
// agents_runs, and lib/validate.js mirrored it. A poller built against that is
|
|
8
|
+
// correct, tested, and unreachable — green tests over a branch nothing can enter.
|
|
9
|
+
//
|
|
10
|
+
// What this locks:
|
|
11
|
+
// * the THREE places that must agree (validator, definitions CHECK, runs CHECK)
|
|
12
|
+
// — the ledger constraint is the one that is easy to forget, and forgetting it
|
|
13
|
+
// yields the worst shape available: an agent that looks armed, fires, and
|
|
14
|
+
// then violates a CHECK when it tries to record the run.
|
|
15
|
+
// * the cadence VOCABULARY, including the sub-hourly divisor rule.
|
|
16
|
+
// * that validate.js's duplicated CADENCES really match scripts/gds/
|
|
17
|
+
// routine-schedule.js's, which is the copy this one was made from.
|
|
18
|
+
// * that widening the enum did not loosen it — an unwired name is still refused.
|
|
19
|
+
|
|
20
|
+
import { test } from 'node:test';
|
|
21
|
+
import assert from 'node:assert/strict';
|
|
22
|
+
import fs from 'node:fs';
|
|
23
|
+
import path from 'node:path';
|
|
24
|
+
import { createRequire } from 'node:module';
|
|
25
|
+
import { fileURLToPath } from 'node:url';
|
|
26
|
+
|
|
27
|
+
const require = createRequire(import.meta.url);
|
|
28
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
29
|
+
const v = require('../modules/agents/lib/validate.js');
|
|
30
|
+
const schedule = require('../scripts/gds/routine-schedule.js');
|
|
31
|
+
|
|
32
|
+
const sql = (f) => fs.readFileSync(path.join(ROOT, 'modules', 'agents', 'migrations', f), 'utf8');
|
|
33
|
+
const mig001 = sql('agents_001_definitions.sql');
|
|
34
|
+
const mig005 = sql('agents_005_cron_trigger.sql');
|
|
35
|
+
|
|
36
|
+
const def = (over = {}) => ({ name: 'ship-digest', persona: 'x'.repeat(40), ...over });
|
|
37
|
+
const cron = (cadence) => def({ trigger_type: 'cron', trigger_spec: { cadence } });
|
|
38
|
+
|
|
39
|
+
// ---- 1. the validator ------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
test("'cron' is a wired trigger type", () => {
|
|
42
|
+
assert.ok(v.TRIGGER_TYPES.includes('cron'));
|
|
43
|
+
// Widening is not loosening: the enum is still closed.
|
|
44
|
+
assert.equal(v.validateAgentDefinition(def({ trigger_type: 'webhook', trigger_spec: {} })).ok, false);
|
|
45
|
+
assert.deepEqual([...v.TRIGGER_TYPES].sort(), ['cron', 'event', 'on-demand']);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('a cron definition must say WHEN, or it can never fire', () => {
|
|
49
|
+
const r = v.validateAgentDefinition(def({ trigger_type: 'cron', trigger_spec: {} }));
|
|
50
|
+
assert.equal(r.ok, false);
|
|
51
|
+
assert.ok(r.errors.some((e) => e.code === 'trigger_cadence_missing'),
|
|
52
|
+
'the missing cadence is named as its own error, not folded into a generic spec failure');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('the cadence vocabulary: named, and sub-hourly where N divides 60', () => {
|
|
56
|
+
for (const good of ['hourly', 'daily', 'weekly', 'monthly', '15min', '5min', '30min', '1min']) {
|
|
57
|
+
assert.equal(v.validateAgentDefinition(cron(good)).ok, true, `${good} is a cadence`);
|
|
58
|
+
}
|
|
59
|
+
// 7 and 45 do not divide 60: `*/7` fires :00…:56 then waits 4, so the declared
|
|
60
|
+
// period and the realized period would be different claims.
|
|
61
|
+
for (const bad of ['7min', '45min', '0min', '60min', 'fortnightly', 'often', '']) {
|
|
62
|
+
assert.equal(v.validateAgentDefinition(cron(bad)).ok, false, `${bad} is not a cadence`);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('an accepted cron definition keeps its cadence through normalization', () => {
|
|
67
|
+
// The bug class task 1004007 documents one axis over: validate accepted a field
|
|
68
|
+
// and then DROPPED it, so a definition looked configured and was not.
|
|
69
|
+
const r = v.validateAgentDefinition(cron('daily'));
|
|
70
|
+
assert.equal(r.ok, true);
|
|
71
|
+
assert.equal(r.value.trigger_spec.cadence, 'daily', 'the cadence survives into the normalized value');
|
|
72
|
+
assert.equal(r.value.trigger_type, 'cron');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('the event path is untouched', () => {
|
|
76
|
+
assert.equal(v.validateAgentDefinition(def({ trigger_type: 'event', trigger_spec: { event: 'task.shipped' } })).ok, true);
|
|
77
|
+
assert.equal(v.validateAgentDefinition(def({ trigger_type: 'event', trigger_spec: {} })).ok, false);
|
|
78
|
+
assert.equal(v.validateAgentDefinition(def({ trigger_type: 'event', trigger_spec: { event: 'nope' } })).ok, false);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('a VALID cadence on a non-cron definition is accepted, as a wired event is', () => {
|
|
82
|
+
// Pinned because the reach is a judgement call and the comment beside the code
|
|
83
|
+
// claims exactly this. An UNWIRED cadence is still refused wherever it appears.
|
|
84
|
+
assert.equal(v.validateAgentDefinition(def({ trigger_type: 'on-demand', trigger_spec: { cadence: 'daily' } })).ok, true);
|
|
85
|
+
assert.equal(v.validateAgentDefinition(def({ trigger_type: 'on-demand', trigger_spec: { cadence: '7min' } })).ok, false);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// ---- 2. the copy that is free to drift -------------------------------------
|
|
89
|
+
|
|
90
|
+
test('validate.js CADENCES agrees with routine-schedule.js, the copy it came from', () => {
|
|
91
|
+
// A module may require only src/module-api.js (ADR 0083), so validate.js cannot
|
|
92
|
+
// import this list — it holds a duplicate. This test is the thing that keeps the
|
|
93
|
+
// duplicate honest; without it the two are one edit away from disagreeing about
|
|
94
|
+
// what a cadence is, and nothing would say so until a tick.
|
|
95
|
+
assert.deepEqual([...v.CADENCES].sort(), [...schedule.CADENCES].sort());
|
|
96
|
+
for (const n of [1, 5, 7, 15, 30, 45, 60, 0]) {
|
|
97
|
+
assert.equal(v.isCadence(`${n}min`), schedule.isCadence(`${n}min`),
|
|
98
|
+
`the two copies agree on ${n}min`);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// ---- 3. both CHECK constraints ---------------------------------------------
|
|
103
|
+
|
|
104
|
+
test('BOTH tables widen — the ledger as well as the definition', () => {
|
|
105
|
+
// agents_001 constrains trigger_type on agents_runs too. Widen only the
|
|
106
|
+
// definition and a cron agent stores, enables, fires, and THEN fails when the
|
|
107
|
+
// run insert violates its own CHECK.
|
|
108
|
+
for (const c of ['agents_definitions_trigger_type_chk', 'agents_runs_trigger_type_chk']) {
|
|
109
|
+
assert.ok(mig001.includes(c), `${c} exists in agents_001 — this test is about widening it`);
|
|
110
|
+
assert.match(mig005, new RegExp(`ADD CONSTRAINT ${c}[\\s\\S]{0,200}?'cron'`),
|
|
111
|
+
`${c} is re-added including 'cron'`);
|
|
112
|
+
assert.match(mig005, new RegExp(`DROP CONSTRAINT ${c}`),
|
|
113
|
+
`${c} is dropped before being re-added — ALTER cannot widen a CHECK in place`);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('the migration pins the SHAPE of a cron spec, leaving the vocabulary to the validator', () => {
|
|
118
|
+
assert.match(mig005, /agents_definitions_trigger_spec_cadence_chk/);
|
|
119
|
+
assert.match(mig005, /trigger_type <> 'cron'/, 'the guard only binds cron rows');
|
|
120
|
+
assert.match(mig005, /trigger_spec \? 'cadence'/, 'and requires the key');
|
|
121
|
+
// The sub-hourly divisor rule must NOT be spelled in SQL — that would be a
|
|
122
|
+
// second copy of isCadence(), free to drift from the one the validator uses.
|
|
123
|
+
assert.ok(!/% *60|divid/i.test(mig005.replace(/^--.*$/gm, '')),
|
|
124
|
+
'no arithmetic cadence rule in the executable SQL — the predicate lives in one place');
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test('the migration is additive, namespaced and idempotent (ADR 0083 §5)', () => {
|
|
128
|
+
assert.match(mig005, /IF (NOT )?EXISTS/, 'guarded so a re-run is safe');
|
|
129
|
+
assert.ok(!/\bDROP TABLE\b|\bDROP COLUMN\b/i.test(mig005), 'nothing is destroyed');
|
|
130
|
+
for (const stmt of mig005.match(/(CREATE|ALTER)\s+TABLE\s+(\w+)/gi) || []) {
|
|
131
|
+
assert.match(stmt, /\s(agents_\w+)$/, `${stmt} touches only agents_-namespaced tables`);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
@@ -164,12 +164,20 @@ await test('an on-demand definition carrying a WIRED event is accepted', () => {
|
|
|
164
164
|
|
|
165
165
|
// --- THE TRIGGER TYPE -------------------------------------------------------
|
|
166
166
|
|
|
167
|
-
await test('TRIGGER_TYPES is the
|
|
168
|
-
|
|
167
|
+
await test('TRIGGER_TYPES is the wired set the DB CHECK also pins', () => {
|
|
168
|
+
// Was the v1 PAIR; task 1004008 added 'cron' so tasks 1002498/1002501 select
|
|
169
|
+
// from a set that can be non-empty. Kept as an exact list on purpose: widening
|
|
170
|
+
// this vocabulary must be a deliberate edit here AND in agents_005's CHECK on
|
|
171
|
+
// both agents_definitions and agents_runs (tests/agents_cron_trigger.mjs pins
|
|
172
|
+
// that the three agree).
|
|
173
|
+
assert.deepEqual(TRIGGER_TYPES, ['event', 'on-demand', 'cron']);
|
|
169
174
|
});
|
|
170
175
|
|
|
171
176
|
await test('an unknown trigger type is refused', () => {
|
|
172
|
-
|
|
177
|
+
// 'cron' was this list's first entry until task 1004008 wired it. 'webhook' and
|
|
178
|
+
// 'schedule' are the plausible-but-unwired names now — the case exists to prove
|
|
179
|
+
// an unwired name is refused, so it needs names that are actually unwired.
|
|
180
|
+
for (const bad of ['webhook', 'schedule', 'Event', 'on demand', '', null, undefined, 7, {}]) {
|
|
173
181
|
const r = validateAgentDefinition({ ...OK_DEF, trigger_type: bad }, OPTS);
|
|
174
182
|
assert.equal(r.ok, false, `${JSON.stringify(bad)} must be refused`);
|
|
175
183
|
assert.ok(hasCode(r, 'trigger_type_unknown'), `${JSON.stringify(bad)} → ${JSON.stringify(codes(r))}`);
|
|
@@ -471,7 +479,7 @@ await test('a hostile getter cannot escape the validator', () => {
|
|
|
471
479
|
|
|
472
480
|
await test('EVERY error carries a code, a field and a human message', () => {
|
|
473
481
|
const r = validateAgentDefinition(
|
|
474
|
-
{ name: 'BAD NAME', trigger_type: '
|
|
482
|
+
{ name: 'BAD NAME', trigger_type: 'webhook', trigger_spec: 'nope', scope_modules: ['government'] },
|
|
475
483
|
{ authorRank: 'xenos', allowedModules: ['agents'] },
|
|
476
484
|
);
|
|
477
485
|
assert.equal(r.ok, false);
|
|
@@ -485,7 +493,7 @@ await test('EVERY error carries a code, a field and a human message', () => {
|
|
|
485
493
|
|
|
486
494
|
await test('validation reports ALL faults at once, not just the first', () => {
|
|
487
495
|
// An author fixing a definition file should not have to re-run four times.
|
|
488
|
-
const r = validateAgentDefinition({ name: 'X', persona: '', trigger_type: '
|
|
496
|
+
const r = validateAgentDefinition({ name: 'X', persona: '', trigger_type: 'webhook' }, OPTS);
|
|
489
497
|
for (const code of ['name_invalid', 'persona_missing', 'trigger_type_unknown']) {
|
|
490
498
|
assert.ok(hasCode(r, code), `${code} missing from ${JSON.stringify(codes(r))}`);
|
|
491
499
|
}
|