cvx-kit 0.1.15 → 0.1.16

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.
Files changed (45) hide show
  1. package/README.md +59 -38
  2. package/dist/relations.mjs +1 -1
  3. package/dist/relations.mjs.map +1 -1
  4. package/dist/zod-table.d.mts +59 -2
  5. package/dist/zod-table.mjs +1 -1
  6. package/dist/zod-table.mjs.map +1 -1
  7. package/package.json +10 -3
  8. package/AGENTS.md +0 -47
  9. package/CHANGELOG.md +0 -337
  10. package/ROADMAP.md +0 -126
  11. package/docs/agent-tools.md +0 -41
  12. package/docs/approvals.md +0 -201
  13. package/docs/architecture.md +0 -124
  14. package/docs/auth.md +0 -443
  15. package/docs/command-context.md +0 -150
  16. package/docs/commands-legacy.md +0 -513
  17. package/docs/commands.md +0 -173
  18. package/docs/conventions.md +0 -345
  19. package/docs/crud.md +0 -66
  20. package/docs/domain-contracts.md +0 -148
  21. package/docs/domain-operations.md +0 -615
  22. package/docs/effect.md +0 -403
  23. package/docs/llms.md +0 -324
  24. package/docs/maintainability.md +0 -124
  25. package/docs/migration.md +0 -351
  26. package/docs/oxlint.md +0 -260
  27. package/docs/querying.md +0 -329
  28. package/docs/tenancy.md +0 -176
  29. package/docs/triggers.md +0 -77
  30. package/docs/upgrading.md +0 -287
  31. package/docs/webhooks.md +0 -58
  32. package/docs/zod-table.md +0 -278
  33. package/scripts/convex-config-bridge.mjs +0 -44
  34. package/scripts/convex-smoke-env.mjs +0 -33
  35. package/scripts/extract-changelog.mjs +0 -29
  36. package/scripts/owned-process.mjs +0 -140
  37. package/scripts/owned-process.test.mjs +0 -107
  38. package/scripts/smoke-packed-components.mjs +0 -341
  39. package/scripts/smoke-packed-effect.mjs +0 -439
  40. package/scripts/smoke-packed-querying.mjs +0 -298
  41. package/scripts/test-packed-auth.mjs +0 -72
  42. package/scripts/test-packed-effect.mjs +0 -482
  43. package/scripts/test-packed-ids.mjs +0 -150
  44. package/scripts/test-packed-oxlint.mjs +0 -167
  45. package/scripts/test-packed-querying.mjs +0 -185
@@ -1,33 +0,0 @@
1
- import { existsSync } from 'node:fs'
2
- import { homedir } from 'node:os'
3
- import { join } from 'node:path'
4
-
5
- /** A smoke fixture must never inherit a cloud or self-hosted deployment selection. */
6
- export function localConvexEnvironment(source = process.env) {
7
- const env = Object.fromEntries(
8
- Object.entries(source).filter(
9
- ([key]) =>
10
- !key.startsWith('CONVEX_') && key !== 'NEXT_PUBLIC_CONVEX_URL' && key !== 'VITE_CONVEX_URL',
11
- ),
12
- )
13
- return { ...env, CONVEX_AGENT_MODE: 'anonymous' }
14
- }
15
-
16
- /** Convex 1.45 can fall back to a legacy home deployment with the same agent name. */
17
- export function assertIsolatedConvexFixture(
18
- fixture,
19
- legacyState = join(homedir(), '.convex', 'anonymous-convex-backend-state', 'anonymous-agent'),
20
- ) {
21
- const localConfig = join(fixture, '.convex', 'local', 'default', 'config.json')
22
- if (!existsSync(localConfig) && existsSync(legacyState)) {
23
- throw new Error(
24
- 'Local smoke cannot reuse legacy anonymous-agent state; an isolated fixture is required',
25
- )
26
- }
27
- }
28
-
29
- export function assertLocalConvexState(fixture) {
30
- if (!existsSync(join(fixture, '.convex', 'local', 'default', 'config.json'))) {
31
- throw new Error('Local smoke did not create project-local deployment state')
32
- }
33
- }
@@ -1,29 +0,0 @@
1
- // Extracts one version's section from CHANGELOG.md (Keep a Changelog form).
2
- // Usage: node scripts/extract-changelog.mjs 0.1.0 [outfile]
3
- // Used by the release workflow for GitHub Release notes, and runnable
4
- // locally BEFORE tagging to verify extraction (see the release plan).
5
- import { readFileSync, writeFileSync } from 'node:fs'
6
-
7
- const version = process.argv[2]
8
- if (!version) {
9
- console.error('usage: extract-changelog.mjs <version> [outfile]')
10
- process.exit(1)
11
- }
12
- const changelog = readFileSync(new URL('../CHANGELOG.md', import.meta.url), 'utf8')
13
- const pattern = new RegExp(
14
- String.raw`^## \[${version.replace(/\./g, String.raw`\.`)}\][^\n]*\n([\s\S]*?)(?=^## \[|(?![\s\S]))`,
15
- 'm',
16
- )
17
- const match = changelog.match(pattern)
18
- if (!match) {
19
- console.error(`CHANGELOG.md has no section for version ${version}`)
20
- process.exit(1)
21
- }
22
- const notes = match[1].trim()
23
- if (!notes) {
24
- console.error(`CHANGELOG.md section for ${version} is empty`)
25
- process.exit(1)
26
- }
27
- const outfile = process.argv[3]
28
- if (outfile) writeFileSync(outfile, `${notes}\n`)
29
- else process.stdout.write(`${notes}\n`)
@@ -1,140 +0,0 @@
1
- import { spawn } from 'node:child_process'
2
- import { setTimeout as delay } from 'node:timers/promises'
3
-
4
- export function processGroupAlive(pid) {
5
- try {
6
- process.kill(-pid, 0)
7
- return true
8
- } catch (error) {
9
- if (error.code === 'ESRCH') return false
10
- throw error
11
- }
12
- }
13
- const owned = new Set()
14
- export async function cleanupOwnedProcess(pid, graceMs = 1000) {
15
- if (!owned.has(pid)) throw new Error('Process group is not owned by this runner')
16
- const signals = [],
17
- errors = []
18
- for (const signal of ['SIGINT', 'SIGTERM', 'SIGKILL']) {
19
- if (!processGroupAlive(pid)) break
20
- try {
21
- process.kill(-pid, signal)
22
- signals.push(signal)
23
- } catch (error) {
24
- if (error.code !== 'ESRCH') errors.push({ signal, code: error.code })
25
- }
26
- const deadline = performance.now() + graceMs
27
- while (processGroupAlive(pid) && performance.now() < deadline) await delay(20)
28
- }
29
- return { pid, groupId: pid, signals, errors, alive: processGroupAlive(pid) }
30
- }
31
-
32
- export async function runOwnedProcess({
33
- command,
34
- args,
35
- cwd,
36
- env,
37
- timeoutMs = 180000,
38
- graceMs = 1000,
39
- onSpawn = () => {},
40
- }) {
41
- const started = performance.now()
42
- const child = spawn(command, args, { cwd, env, detached: true, stdio: ['pipe', 'pipe', 'pipe'] })
43
- let stdout = '',
44
- stderr = '',
45
- timedOut = false,
46
- spawnError,
47
- timeoutCleanup
48
- child.stdout.on('data', (bytes) => {
49
- stdout += bytes
50
- })
51
- child.stderr.on('data', (bytes) => {
52
- stderr += bytes
53
- })
54
- const closed = new Promise((resolve) => {
55
- child.once('error', (error) => {
56
- spawnError = { code: error.code, message: error.message }
57
- resolve({ code: null, signal: null })
58
- })
59
- child.once('close', (code, signal) => resolve({ code, signal }))
60
- })
61
- if (child.pid) {
62
- owned.add(child.pid)
63
- onSpawn(child.pid)
64
- }
65
- child.stdin.end()
66
- let finishTimeout
67
- const timeoutFinished = new Promise((resolve) => {
68
- finishTimeout = resolve
69
- })
70
- const timer = setTimeout(() => {
71
- timedOut = true
72
- timeoutCleanup = child.pid
73
- ? cleanupOwnedProcess(child.pid, graceMs).catch((error) => ({
74
- pid: child.pid,
75
- alive: true,
76
- error: { code: error.code, message: error.message },
77
- }))
78
- : Promise.resolve(null)
79
- timeoutCleanup.then(() => finishTimeout({ code: child.exitCode, signal: child.signalCode }))
80
- }, timeoutMs)
81
- let exit, cleanup
82
- try {
83
- exit = await Promise.race([closed, timeoutFinished])
84
- if (timeoutCleanup) await timeoutCleanup
85
- } finally {
86
- clearTimeout(timer)
87
- if (child.pid) {
88
- try {
89
- cleanup = await cleanupOwnedProcess(child.pid, graceMs)
90
- } catch (error) {
91
- cleanup = {
92
- pid: child.pid,
93
- alive: true,
94
- error: { code: error.code, message: error.message },
95
- }
96
- }
97
- }
98
- }
99
- if (timedOut) {
100
- child.stdout.destroy()
101
- child.stderr.destroy()
102
- child.unref()
103
- }
104
- return {
105
- ...exit,
106
- spawnError,
107
- stdout,
108
- stderr,
109
- timedOut,
110
- pid: child.pid,
111
- milliseconds: performance.now() - started,
112
- timeoutCleanup: await timeoutCleanup,
113
- cleanup,
114
- }
115
- }
116
-
117
- export async function collectOwnedCleanup({ pids, ports, checkPort, failure, graceMs = 1000 }) {
118
- const groups = []
119
- for (const pid of pids) {
120
- try {
121
- groups.push(await cleanupOwnedProcess(pid, graceMs))
122
- } catch (error) {
123
- groups.push({ pid, alive: true, error: { code: error.code, message: error.message } })
124
- }
125
- }
126
- const portResults = []
127
- for (const port of ports) {
128
- try {
129
- portResults.push({ port, closed: (await checkPort(port)) === port })
130
- } catch (error) {
131
- portResults.push({ port, closed: false, error: { code: error.code, message: error.message } })
132
- }
133
- }
134
- return {
135
- pids: groups.map(({ pid, alive }) => ({ pid, alive })),
136
- failure: failure ? { name: failure.name, message: failure.message } : null,
137
- groups,
138
- ports: portResults,
139
- }
140
- }
@@ -1,107 +0,0 @@
1
- // @vitest-environment node
2
- import assert from 'node:assert/strict'
3
- import { test } from 'vite-plus/test'
4
- import { createServer } from 'node:net'
5
- import { mkdtempSync, writeFileSync } from 'node:fs'
6
- import { join } from 'node:path'
7
- import {
8
- runOwnedProcess,
9
- cleanupOwnedProcess,
10
- processGroupAlive,
11
- collectOwnedCleanup,
12
- } from './owned-process.mjs'
13
-
14
- const proof = mkdtempSync('/tmp/cvx-owned-process-')
15
- console.log('Owned cleanup evidence: ' + proof)
16
-
17
- const descendant = `process.on('SIGINT',()=>{}); process.on('SIGTERM',()=>{}); console.log('ready'); setInterval(()=>{},1000)`
18
- const parent = `
19
- const {spawn}=require('node:child_process');
20
- const child=spawn(process.execPath,['-e',${JSON.stringify(descendant)}],{stdio:['ignore','pipe','ignore']});
21
- process.on('SIGINT',()=>{}); process.on('SIGTERM',()=>{});
22
- child.stdout.once('data',()=>{console.log('descendant:'+child.pid); MODE});
23
- setInterval(()=>{},1000);
24
- `
25
-
26
- test('timeout terminates the entire owned group after ignored graceful signals', async () => {
27
- let pid
28
- try {
29
- const result = await runOwnedProcess({
30
- command: process.execPath,
31
- args: ['-e', parent.replace('MODE', '')],
32
- timeoutMs: 1500,
33
- graceMs: 150,
34
- onSpawn: (value) => {
35
- pid = value
36
- },
37
- })
38
- writeFileSync(join(proof, 'timeout.json'), JSON.stringify(result, null, 2))
39
- assert.match(result.stdout, /descendant:\d+/)
40
- assert.equal(result.timedOut, true)
41
- assert.ok(result.timeoutCleanup.signals.includes('SIGKILL'))
42
- assert.equal(result.cleanup.alive, false)
43
- assert.equal(processGroupAlive(pid), false)
44
- } finally {
45
- if (pid) await cleanupOwnedProcess(pid, 150)
46
- }
47
- })
48
-
49
- test('failed leader preserves failure output and finally cleans its surviving descendant', async () => {
50
- let pid
51
- try {
52
- const result = await runOwnedProcess({
53
- command: process.execPath,
54
- args: ['-e', parent.replace('MODE', "console.error('original failure'); process.exit(7)")],
55
- timeoutMs: 1500,
56
- graceMs: 150,
57
- onSpawn: (value) => {
58
- pid = value
59
- },
60
- })
61
- writeFileSync(join(proof, 'failed-leader.json'), JSON.stringify(result, null, 2))
62
- assert.equal(result.code, 7)
63
- assert.equal(result.timedOut, false)
64
- assert.match(result.stderr, /original failure/)
65
- assert.equal(result.cleanup.alive, false)
66
- assert.equal(processGroupAlive(pid), false)
67
- } finally {
68
- if (pid) await cleanupOwnedProcess(pid, 150)
69
- }
70
- })
71
-
72
- test('cleanup rejects a group that was not created by the runner', async () => {
73
- await assert.rejects(cleanupOwnedProcess(process.pid, 20), /not owned/)
74
- })
75
-
76
- test('port-check failure is recorded without replacing original proof failure', async () => {
77
- const server = createServer()
78
- await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve))
79
- const port = server.address().port
80
- try {
81
- const result = await runOwnedProcess({
82
- command: process.execPath,
83
- args: ['-e', 'process.exit(7)'],
84
- timeoutMs: 1000,
85
- })
86
- const failure = new Error('original proof failure: exit ' + result.code)
87
- const evidence = await collectOwnedCleanup({
88
- pids: [result.pid],
89
- ports: [port],
90
- failure,
91
- checkPort: (requested) =>
92
- new Promise((resolve, reject) => {
93
- const probe = createServer()
94
- probe.once('error', reject)
95
- probe.listen(requested, '127.0.0.1', () => probe.close(() => resolve(requested)))
96
- }),
97
- })
98
- writeFileSync(join(proof, 'port-failure.json'), JSON.stringify(evidence, null, 2))
99
- assert.equal(evidence.failure.message, 'original proof failure: exit 7')
100
- assert.equal(evidence.ports[0].closed, false)
101
- assert.equal(evidence.ports[0].error.code, 'EADDRINUSE')
102
- assert.equal(evidence.groups[0].alive, false)
103
- assert.equal(server.listening, true, 'unrelated listener must never be killed')
104
- } finally {
105
- await new Promise((resolve) => server.close(resolve))
106
- }
107
- })
@@ -1,341 +0,0 @@
1
- import { execFileSync } from 'node:child_process'
2
- import { copyFileSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
3
- import { tmpdir } from 'node:os'
4
- import { join } from 'node:path'
5
- import {
6
- assertIsolatedConvexFixture,
7
- assertLocalConvexState,
8
- localConvexEnvironment,
9
- } from './convex-smoke-env.mjs'
10
-
11
- const installer = process.argv[2] ?? 'bun'
12
- if (installer !== 'bun' && installer !== 'npm') {
13
- throw new Error('Usage: smoke-packed-components.mjs [bun|npm]')
14
- }
15
-
16
- const root = join(import.meta.dirname, '..')
17
- const temporaryRoot = mkdtempSync(join(tmpdir(), 'cvx-kit-smoke-'))
18
- const fixture = join(temporaryRoot, 'fixture')
19
- const packageManifest = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'))
20
- const packageVersion = packageManifest.version
21
- const tarball = join(temporaryRoot, `cvx-kit-${packageVersion}.tgz`)
22
-
23
- function run(command, args, cwd = fixture) {
24
- return execFileSync(command, args, {
25
- cwd,
26
- encoding: 'utf8',
27
- stdio: ['ignore', 'pipe', 'pipe'],
28
- env: localConvexEnvironment(),
29
- })
30
- }
31
-
32
- function runStreaming(command, args, cwd = fixture) {
33
- execFileSync(command, args, {
34
- cwd,
35
- stdio: 'inherit',
36
- env: localConvexEnvironment(),
37
- })
38
- }
39
-
40
- function runConvex(...args) {
41
- return run('node', [join(fixture, 'node_modules', 'convex', 'bin', 'main.js'), ...args])
42
- }
43
-
44
- function write(relativePath, contents) {
45
- const path = join(fixture, relativePath)
46
- writeFileSync(path, contents)
47
- }
48
-
49
- function step(label, callback) {
50
- console.log(`smoke step: ${label}`)
51
- return callback()
52
- }
53
-
54
- try {
55
- run('bun', ['pm', 'pack', '--destination', temporaryRoot, '--ignore-scripts'], root)
56
- mkdirSync(join(fixture, 'convex'), { recursive: true })
57
- write(
58
- 'package.json',
59
- JSON.stringify(
60
- {
61
- private: true,
62
- type: 'module',
63
- dependencies: {
64
- 'cvx-kit': `file:${tarball}`,
65
- convex: packageManifest.devDependencies.convex,
66
- zod: packageManifest.devDependencies.zod,
67
- 'convex-helpers': packageManifest.devDependencies['convex-helpers'],
68
- },
69
- devDependencies: {
70
- 'vite-plus': packageManifest.devDependencies['vite-plus'],
71
- },
72
- },
73
- null,
74
- 2,
75
- ),
76
- )
77
- write(
78
- 'packed-test-helper.test.ts',
79
- `import { describe, expect, it } from 'vite-plus/test'
80
- import { registerApprovals, registerFoundation } from 'cvx-kit/test'
81
-
82
- describe('packed cvx-kit test helpers', () => {
83
- it('registers both compiled component bundles and their discoverable schemas', () => {
84
- const registrations = []
85
- const testRuntime = {
86
- registerComponent(name, schema, modules) {
87
- registrations.push({ name, schema, modules })
88
- },
89
- }
90
-
91
- registerApprovals(testRuntime)
92
- registerFoundation(testRuntime)
93
-
94
- expect(registrations.map(({ name }) => name)).toEqual(['approvals', 'foundation'])
95
- expect(Object.keys(registrations[0].schema.tables).sort()).toEqual([
96
- 'approvalDecisions',
97
- 'approvalRuns',
98
- ])
99
- expect(Object.keys(registrations[0].modules)).toContain(
100
- '../dist/components/approvals/health.mjs',
101
- )
102
- })
103
- })
104
- `,
105
- )
106
- write(
107
- 'convex/convex.config.ts',
108
- `import { defineApp } from 'convex/server'
109
- import approvals from 'cvx-kit/components/approvals/convex.config'
110
- import foundation from 'cvx-kit/components/foundation/convex.config'
111
-
112
- const app = defineApp()
113
- app.use(approvals)
114
- app.use(foundation)
115
- export default app
116
- `,
117
- )
118
- write(
119
- 'convex/smoke.ts',
120
- `import { approvalCallbackArgs } from 'cvx-kit/components/approvals'
121
- import { createFunctionHandle, paginationOptsValidator } from 'convex/server'
122
- import { v } from 'convex/values'
123
- import { components, internal } from './_generated/api'
124
- import { internalMutation, mutation, query } from './_generated/server'
125
-
126
- export const applyDecision = internalMutation({
127
- args: approvalCallbackArgs,
128
- returns: v.null(),
129
- handler: (_ctx, input) => {
130
- if (input.decision?.evidence[0]?.stepKey !== 'releaseDecision') {
131
- throw new Error('Missing callback decision step key')
132
- }
133
- return null
134
- },
135
- })
136
-
137
- export const rejectDecision = internalMutation({
138
- args: approvalCallbackArgs,
139
- returns: v.null(),
140
- handler: () => null,
141
- })
142
-
143
- export const health = query({
144
- args: {},
145
- returns: v.any(),
146
- handler: (ctx) => ctx.runQuery(components.approvals.health.check, {}),
147
- })
148
-
149
- export const foundationHealth = query({
150
- args: {},
151
- returns: v.literal('ready'),
152
- handler: (ctx) => ctx.runQuery(components.foundation.functions.status, {}),
153
- })
154
-
155
- export const start = mutation({
156
- args: { resourceRef: v.optional(v.string()) },
157
- returns: v.object({ runId: v.string() }),
158
- handler: async (ctx, args) => {
159
- const approvedHandle = await createFunctionHandle(internal.smoke.applyDecision)
160
- const rejectedHandle = await createFunctionHandle(internal.smoke.rejectDecision)
161
- return ctx.runMutation(components.approvals.requests.start, {
162
- scopeRef: 'smoke',
163
- resourceType: 'release',
164
- resourceRef: args.resourceRef ?? 'packed-component',
165
- requester: { actorRef: 'requester', capabilities: [] },
166
- workflow: {
167
- schemaVersion: 1,
168
- compatibilityKey: 'packedSmoke',
169
- name: 'packedSmoke',
170
- steps: [
171
- {
172
- kind: 'decision',
173
- key: 'releaseDecision',
174
- decisions: ['approved', 'rejected'],
175
- quorum: { kind: 'count', approvals: 1 },
176
- makerChecker: true,
177
- },
178
- {
179
- kind: 'branch',
180
- key: 'routeDecision',
181
- approvedStepKey: 'applyDecision',
182
- rejectedStepKey: 'rejectDecision',
183
- },
184
- {
185
- kind: 'mutation',
186
- key: 'applyDecision',
187
- callback: { kind: 'mutation', handle: approvedHandle, retry: false },
188
- },
189
- {
190
- kind: 'mutation',
191
- key: 'rejectDecision',
192
- callback: { kind: 'mutation', handle: rejectedHandle, retry: false },
193
- },
194
- ],
195
- },
196
- })
197
- },
198
- })
199
-
200
- export const list = query({
201
- args: {
202
- paginationOpts: paginationOptsValidator,
203
- state: v.optional(v.union(v.literal('pending'), v.literal('approved'))),
204
- },
205
- returns: v.any(),
206
- handler: (ctx, args) => ctx.runQuery(components.approvals.requests.list, {
207
- scopeRef: 'smoke',
208
- ...args,
209
- }),
210
- })
211
-
212
- export const decide = mutation({
213
- args: { runId: v.string() },
214
- returns: v.any(),
215
- handler: (ctx, args) => ctx.runMutation(components.approvals.decisions.decide, {
216
- runId: args.runId,
217
- decision: 'approved',
218
- actor: { actorRef: 'approver', capabilities: [] },
219
- compatibilityKey: 'packedSmoke',
220
- }),
221
- })
222
-
223
- export const history = query({
224
- args: { runId: v.string() },
225
- returns: v.any(),
226
- handler: (ctx, args) => ctx.runQuery(components.approvals.decisions.list, args),
227
- })
228
-
229
- export const status = query({
230
- args: { runId: v.string() },
231
- returns: v.any(),
232
- handler: (ctx, args) => ctx.runQuery(components.approvals.requests.status, {
233
- ...args,
234
- compatibilityKey: 'packedSmoke',
235
- }),
236
- })
237
- `,
238
- )
239
-
240
- if (installer === 'bun') {
241
- copyFileSync(join(root, 'bun.lock'), join(fixture, 'bun.lock'))
242
- }
243
- if (installer === 'bun')
244
- step('install with bun', () => runStreaming('bun', ['install', '--ignore-scripts']))
245
- else step('install with npm', () => runStreaming('npm', ['install', '--ignore-scripts']))
246
- if (installer === 'bun')
247
- step('test helper with bun', () =>
248
- runStreaming('bunx', ['vp', 'test', 'run', 'packed-test-helper.test.ts']),
249
- )
250
- else
251
- step('test helper with npm', () =>
252
- runStreaming('npx', ['vp', 'test', 'run', 'packed-test-helper.test.ts']),
253
- )
254
-
255
- assertIsolatedConvexFixture(fixture)
256
- step('deploy Convex fixture', () => runConvex('dev', '--once', '--typecheck=disable'))
257
- assertLocalConvexState(fixture)
258
- const health = JSON.parse(runConvex('run', 'smoke:health'))
259
- if (health.status !== 'ready' || health.schemaVersion !== 1) {
260
- throw new Error(`Unexpected approvals health response: ${JSON.stringify(health)}`)
261
- }
262
- const foundationHealth = JSON.parse(runConvex('run', 'smoke:foundationHealth'))
263
- if (foundationHealth !== 'ready') {
264
- throw new Error(`Unexpected foundation health response: ${JSON.stringify(foundationHealth)}`)
265
- }
266
- const { runId } = JSON.parse(runConvex('run', 'smoke:start'))
267
- const { runId: secondRunId } = JSON.parse(
268
- runConvex('run', 'smoke:start', JSON.stringify({ resourceRef: 'packed-component-2' })),
269
- )
270
- const firstPage = JSON.parse(
271
- runConvex(
272
- 'run',
273
- 'smoke:list',
274
- JSON.stringify({
275
- state: 'pending',
276
- paginationOpts: { cursor: null, numItems: 1 },
277
- }),
278
- ),
279
- )
280
- if (firstPage.page.length !== 1 || firstPage.page[0]._id !== secondRunId) {
281
- throw new Error(`Unexpected first approval page: ${JSON.stringify(firstPage)}`)
282
- }
283
- const secondPage = JSON.parse(
284
- runConvex(
285
- 'run',
286
- 'smoke:list',
287
- JSON.stringify({
288
- state: 'pending',
289
- paginationOpts: {
290
- cursor: firstPage.continueCursor,
291
- numItems: 10,
292
- },
293
- }),
294
- ),
295
- )
296
- if (
297
- secondPage.page.length !== 1 ||
298
- secondPage.page[0]._id !== runId ||
299
- secondPage.page[0]._id === firstPage.page[0]._id
300
- ) {
301
- throw new Error(`Unexpected continuation approval page: ${JSON.stringify(secondPage)}`)
302
- }
303
-
304
- // start schedules the workflow; a successful start is not decision readiness.
305
- let readyStatus
306
- const readyDeadline = Date.now() + 60_000
307
- while (Date.now() < readyDeadline) {
308
- readyStatus = JSON.parse(runConvex('run', 'smoke:status', JSON.stringify({ runId })))
309
- if (
310
- readyStatus?.run.state !== 'pending' ||
311
- ['failed', 'canceled', 'completed'].includes(readyStatus?.execution?.type)
312
- ) {
313
- throw new Error(`Approval ended before decision readiness: ${JSON.stringify(readyStatus)}`)
314
- }
315
- if (readyStatus.run.currentStepKey === 'releaseDecision') break
316
- await new Promise((resolve) => setTimeout(resolve, 500))
317
- }
318
- if (readyStatus?.run.currentStepKey !== 'releaseDecision') {
319
- throw new Error(`Approval did not become decision-ready: ${JSON.stringify(readyStatus)}`)
320
- }
321
- const decision = JSON.parse(runConvex('run', 'smoke:decide', JSON.stringify({ runId })))
322
- if (decision.state !== 'approved') {
323
- throw new Error(`Unexpected decision response: ${JSON.stringify(decision)}`)
324
- }
325
- const history = JSON.parse(runConvex('run', 'smoke:history', JSON.stringify({ runId })))
326
- if (history.length !== 1 || history[0].decision !== 'approved') {
327
- throw new Error(`Unexpected approval history: ${JSON.stringify(history)}`)
328
- }
329
- let status
330
- for (let attempt = 0; attempt < 20; attempt += 1) {
331
- status = JSON.parse(runConvex('run', 'smoke:status', JSON.stringify({ runId })))
332
- if (status.execution?.type === 'completed') break
333
- await new Promise((resolve) => setTimeout(resolve, 500))
334
- }
335
- if (status?.execution?.type !== 'completed') {
336
- throw new Error(`Approval callback did not complete: ${JSON.stringify(status)}`)
337
- }
338
- console.log(`packed component smoke passed with ${installer}`)
339
- } finally {
340
- rmSync(temporaryRoot, { recursive: true, force: true })
341
- }