@checkstack/automation-backend 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +544 -0
- package/drizzle/0003_sparkling_xorn.sql +17 -0
- package/drizzle/0004_cultured_spyke.sql +2 -0
- package/drizzle/0005_classy_the_hand.sql +19 -0
- package/drizzle/0006_burly_wallop.sql +10 -0
- package/drizzle/0007_nappy_jackal.sql +1 -0
- package/drizzle/0008_remove_seeded_auto_incident_automations.sql +13 -0
- package/drizzle/0009_steady_liz_osborn.sql +12 -0
- package/drizzle/0010_chunky_changeling.sql +2 -0
- package/drizzle/meta/0003_snapshot.json +1007 -0
- package/drizzle/meta/0004_snapshot.json +1028 -0
- package/drizzle/meta/0005_snapshot.json +1164 -0
- package/drizzle/meta/0006_snapshot.json +1261 -0
- package/drizzle/meta/0007_snapshot.json +1215 -0
- package/drizzle/meta/0008_snapshot.json +1215 -0
- package/drizzle/meta/0009_snapshot.json +1328 -0
- package/drizzle/meta/0010_snapshot.json +1349 -0
- package/drizzle/meta/_journal.json +56 -0
- package/package.json +23 -12
- package/src/action-types.ts +23 -0
- package/src/artifact-store.ts +16 -1
- package/src/automation-store.test.ts +143 -0
- package/src/automation-store.ts +30 -8
- package/src/builtin-triggers.test.ts +77 -74
- package/src/builtin-triggers.ts +105 -108
- package/src/dispatch/action-kind.ts +2 -0
- package/src/dispatch/assemble-get-service.ts +31 -0
- package/src/dispatch/cancel-resurrect.test.ts +147 -0
- package/src/dispatch/concurrency-race.test.ts +255 -0
- package/src/dispatch/concurrency-scope.test.ts +166 -0
- package/src/dispatch/condition.ts +24 -5
- package/src/dispatch/dwell-queue.ts +65 -0
- package/src/dispatch/dwell-store.ts +154 -0
- package/src/dispatch/dwell.it.test.ts +142 -0
- package/src/dispatch/dwell.test.ts +799 -0
- package/src/dispatch/dwell.ts +257 -0
- package/src/dispatch/engine.test.ts +189 -2
- package/src/dispatch/engine.ts +555 -9
- package/src/dispatch/entity-scope.test.ts +176 -0
- package/src/dispatch/get-service-wiring.test.ts +318 -0
- package/src/dispatch/numeric.test.ts +71 -0
- package/src/dispatch/numeric.ts +96 -0
- package/src/dispatch/render.test.ts +34 -0
- package/src/dispatch/render.ts +31 -11
- package/src/dispatch/reseed-run-secrets.ts +230 -0
- package/src/dispatch/run-secret-registry.test.ts +189 -0
- package/src/dispatch/run-secret-registry.ts +247 -0
- package/src/dispatch/run-state-masking.test.ts +376 -0
- package/src/dispatch/run-state-store.ts +95 -38
- package/src/dispatch/run-state.ts +226 -59
- package/src/dispatch/scope-artifact-masking.test.ts +138 -0
- package/src/dispatch/secret-ref-ids.test.ts +19 -0
- package/src/dispatch/secret-ref-ids.ts +17 -0
- package/src/dispatch/snapshots.test.ts +86 -0
- package/src/dispatch/snapshots.ts +79 -0
- package/src/dispatch/stage1-router.test.ts +324 -0
- package/src/dispatch/stage1-router.ts +152 -0
- package/src/dispatch/stage1.it.test.ts +84 -0
- package/src/dispatch/stage2-dispatch.test.ts +285 -0
- package/src/dispatch/stage2-dispatch.ts +207 -0
- package/src/dispatch/stage2-stalled.it.test.ts +132 -0
- package/src/dispatch/stalled-sweeper.test.ts +197 -0
- package/src/dispatch/stalled-sweeper.ts +112 -5
- package/src/dispatch/state-scope.test.ts +234 -0
- package/src/dispatch/state-scope.ts +322 -0
- package/src/dispatch/structured-conditions.test.ts +246 -0
- package/src/dispatch/structured-conditions.ts +146 -0
- package/src/dispatch/test-fixtures.ts +306 -38
- package/src/dispatch/trigger-fanin.test.ts +111 -0
- package/src/dispatch/trigger-subscriber.ts +316 -14
- package/src/dispatch/types.ts +263 -8
- package/src/dispatch/wait-timeout-queue.ts +89 -0
- package/src/dispatch/wait-until-entity-wake.test.ts +544 -0
- package/src/dispatch/wait-until.test.ts +540 -0
- package/src/dispatch/wake-refs.test.ts +158 -0
- package/src/dispatch/wake-refs.ts +348 -0
- package/src/dispatch/window-gate.test.ts +513 -0
- package/src/dispatch/window-store.test.ts +162 -0
- package/src/dispatch/window-store.ts +102 -0
- package/src/entity/change-derivers.test.ts +148 -0
- package/src/entity/change-derivers.ts +143 -0
- package/src/entity/change-emitter.test.ts +66 -0
- package/src/entity/change-emitter.ts +76 -0
- package/src/entity/create-handle.ts +344 -0
- package/src/entity/cross-pod-read-consistency.it.test.ts +281 -0
- package/src/entity/define-entity.ts +157 -0
- package/src/entity/diff.test.ts +57 -0
- package/src/entity/diff.ts +54 -0
- package/src/entity/entity-store.test.ts +30 -0
- package/src/entity/entity-store.ts +171 -0
- package/src/entity/extension-point.ts +56 -0
- package/src/entity/fake-entity-store.ts +130 -0
- package/src/entity/hook.ts +19 -0
- package/src/entity/index.ts +50 -0
- package/src/entity/mutate-handle.test.ts +517 -0
- package/src/entity/on-entity-changed.test.ts +189 -0
- package/src/entity/on-entity-changed.ts +214 -0
- package/src/entity/registry.test.ts +181 -0
- package/src/entity/registry.ts +200 -0
- package/src/entity/stable-stringify.test.ts +55 -0
- package/src/entity/stable-stringify.ts +49 -0
- package/src/entity/wake-index.it.test.ts +251 -0
- package/src/entity/with-entity-write.test.ts +100 -0
- package/src/entity/with-entity-write.ts +69 -0
- package/src/entity-driven-trigger.ts +46 -0
- package/src/extension-points.ts +35 -0
- package/src/gitops-docs.test.ts +215 -0
- package/src/gitops-docs.ts +151 -0
- package/src/gitops-kinds.test.ts +174 -0
- package/src/gitops-kinds.ts +137 -0
- package/src/index.ts +355 -11
- package/src/migration/flapping-to-window.test.ts +123 -0
- package/src/migration/flapping-to-window.ts +205 -0
- package/src/router.test.ts +182 -1
- package/src/router.ts +73 -2
- package/src/schema.ts +236 -3
- package/src/script-test-replay.test.ts +88 -0
- package/src/script-test-replay.ts +100 -0
- package/src/script-test-shell-env.test.ts +41 -0
- package/src/script-test-shell-env.ts +89 -0
- package/src/script-test.test.ts +386 -0
- package/src/script-test.ts +258 -0
- package/src/trigger-registry.ts +2 -0
- package/src/validate-definition.test.ts +1 -0
- package/tsconfig.json +24 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration test (real Postgres) for the dwell-store atomic claim.
|
|
3
|
+
*
|
|
4
|
+
* Part of the surgical integration lane (plan §14.4 #2). It pins the one
|
|
5
|
+
* behaviour fakes cannot model: the `DELETE … RETURNING` claim in
|
|
6
|
+
* `dwell-store.ts` must be atomic, so that two concurrent `delete(id)` calls
|
|
7
|
+
* (two pods, or the sweeper racing the queue consumer) result in EXACTLY ONE
|
|
8
|
+
* returning a row. That is what stops the same dwell from firing twice.
|
|
9
|
+
*
|
|
10
|
+
* Gated behind `CHECKSTACK_IT=1` so the default `bun test` never runs it. The
|
|
11
|
+
* `integration` CI job sets the flag and provides a real Postgres service
|
|
12
|
+
* container. Connection comes from `CHECKSTACK_IT_PG_URL` (defaulting to the
|
|
13
|
+
* `docker-compose-dev.yml` Postgres port).
|
|
14
|
+
*
|
|
15
|
+
* Each run isolates itself in a freshly created Postgres schema (set as the
|
|
16
|
+
* pool's `search_path`) so it never collides with a real deployment's tables
|
|
17
|
+
* and cleans up after itself. The two tables are created with minimal DDL
|
|
18
|
+
* that mirrors only the columns the dwell store reads/writes.
|
|
19
|
+
*/
|
|
20
|
+
import { afterAll, beforeAll, describe, expect, it } from "bun:test";
|
|
21
|
+
import { drizzle } from "drizzle-orm/node-postgres";
|
|
22
|
+
import { Pool } from "pg";
|
|
23
|
+
|
|
24
|
+
import { automationDwellTimers } from "../schema";
|
|
25
|
+
import { createDwellStore } from "./dwell-store";
|
|
26
|
+
|
|
27
|
+
const PG_URL =
|
|
28
|
+
process.env.CHECKSTACK_IT_PG_URL ??
|
|
29
|
+
"postgres://postgres:postgres@localhost:5432/postgres";
|
|
30
|
+
|
|
31
|
+
// Unique, identifier-safe schema name for this run (no interpolation into DDL
|
|
32
|
+
// beyond this controlled value).
|
|
33
|
+
const SCHEMA = `it_dwell_${crypto.randomUUID().replace(/-/g, "")}`;
|
|
34
|
+
|
|
35
|
+
describe.skipIf(!process.env.CHECKSTACK_IT)(
|
|
36
|
+
"dwell-store atomic claim (real Postgres)",
|
|
37
|
+
() => {
|
|
38
|
+
let pool: Pool;
|
|
39
|
+
|
|
40
|
+
beforeAll(async () => {
|
|
41
|
+
// First, create the schema on a plain connection.
|
|
42
|
+
const setupPool = new Pool({ connectionString: PG_URL });
|
|
43
|
+
try {
|
|
44
|
+
await setupPool.query(`CREATE SCHEMA IF NOT EXISTS "${SCHEMA}"`);
|
|
45
|
+
await setupPool.query(`SET search_path TO "${SCHEMA}"`);
|
|
46
|
+
// Minimal parent table for the FK reference.
|
|
47
|
+
await setupPool.query(`
|
|
48
|
+
CREATE TABLE "${SCHEMA}".automations (
|
|
49
|
+
id text PRIMARY KEY,
|
|
50
|
+
name text NOT NULL,
|
|
51
|
+
status text NOT NULL DEFAULT 'enabled',
|
|
52
|
+
definition jsonb NOT NULL,
|
|
53
|
+
created_at timestamp NOT NULL DEFAULT now(),
|
|
54
|
+
updated_at timestamp NOT NULL DEFAULT now()
|
|
55
|
+
)
|
|
56
|
+
`);
|
|
57
|
+
// The dwell-timer table the store operates on.
|
|
58
|
+
await setupPool.query(`
|
|
59
|
+
CREATE TABLE "${SCHEMA}".automation_dwell_timers (
|
|
60
|
+
id text PRIMARY KEY,
|
|
61
|
+
automation_id text NOT NULL
|
|
62
|
+
REFERENCES "${SCHEMA}".automations(id) ON DELETE CASCADE,
|
|
63
|
+
trigger_id text NOT NULL,
|
|
64
|
+
event_id text NOT NULL,
|
|
65
|
+
context_key text,
|
|
66
|
+
armed_status text,
|
|
67
|
+
payload_snapshot jsonb NOT NULL,
|
|
68
|
+
actor_snapshot jsonb NOT NULL,
|
|
69
|
+
fire_at timestamp NOT NULL,
|
|
70
|
+
created_at timestamp NOT NULL DEFAULT now()
|
|
71
|
+
)
|
|
72
|
+
`);
|
|
73
|
+
await setupPool.query(`
|
|
74
|
+
CREATE UNIQUE INDEX automation_dwell_timers_key_unique
|
|
75
|
+
ON "${SCHEMA}".automation_dwell_timers
|
|
76
|
+
(automation_id, trigger_id, context_key)
|
|
77
|
+
`);
|
|
78
|
+
} finally {
|
|
79
|
+
await setupPool.end();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Pin every pooled connection to the test schema via search_path so the
|
|
83
|
+
// drizzle queries (which run on arbitrary pooled connections) all see
|
|
84
|
+
// our tables.
|
|
85
|
+
pool = new Pool({
|
|
86
|
+
connectionString: PG_URL,
|
|
87
|
+
options: `-c search_path=${SCHEMA}`,
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
afterAll(async () => {
|
|
92
|
+
await pool.end();
|
|
93
|
+
const cleanupPool = new Pool({ connectionString: PG_URL });
|
|
94
|
+
try {
|
|
95
|
+
await cleanupPool.query(`DROP SCHEMA IF EXISTS "${SCHEMA}" CASCADE`);
|
|
96
|
+
} finally {
|
|
97
|
+
await cleanupPool.end();
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("two concurrent delete(id) calls → exactly one returns a row", async () => {
|
|
102
|
+
const db = drizzle({
|
|
103
|
+
client: pool,
|
|
104
|
+
schema: { automationDwellTimers },
|
|
105
|
+
});
|
|
106
|
+
const store = createDwellStore(db);
|
|
107
|
+
|
|
108
|
+
const automationId = crypto.randomUUID();
|
|
109
|
+
await pool.query(
|
|
110
|
+
`INSERT INTO "${SCHEMA}".automations (id, name, definition)
|
|
111
|
+
VALUES ($1, $2, $3)`,
|
|
112
|
+
[automationId, "IT dwell automation", JSON.stringify({})],
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
// Arm a single dwell to claim.
|
|
116
|
+
const armed = await store.arm({
|
|
117
|
+
automationId,
|
|
118
|
+
triggerId: "trigger-1",
|
|
119
|
+
eventId: "incident.incident.created",
|
|
120
|
+
contextKey: "sys-1",
|
|
121
|
+
armedStatus: "degraded",
|
|
122
|
+
payloadSnapshot: { hello: "world" },
|
|
123
|
+
actorSnapshot: { kind: "system" },
|
|
124
|
+
fireAt: new Date(Date.now() + 60_000),
|
|
125
|
+
});
|
|
126
|
+
expect(armed.created).toBe(true);
|
|
127
|
+
|
|
128
|
+
// Fire two concurrent claims for the SAME id. Exactly one must win.
|
|
129
|
+
const [a, b] = await Promise.all([
|
|
130
|
+
store.delete(armed.id),
|
|
131
|
+
store.delete(armed.id),
|
|
132
|
+
]);
|
|
133
|
+
|
|
134
|
+
const winners = [a, b].filter(Boolean);
|
|
135
|
+
expect(winners).toHaveLength(1);
|
|
136
|
+
|
|
137
|
+
// And the row is gone — a third claim finds nothing.
|
|
138
|
+
const third = await store.delete(armed.id);
|
|
139
|
+
expect(third).toBe(false);
|
|
140
|
+
});
|
|
141
|
+
},
|
|
142
|
+
);
|