cvx-kit 0.1.6 → 0.1.8

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 (116) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +63 -38
  3. package/dist/agent-tools.d.mts +2 -1
  4. package/dist/agent-tools.mjs +1 -1
  5. package/dist/agent-tools.mjs.map +1 -1
  6. package/dist/components/approvals/audit.mjs +1 -1
  7. package/dist/components/foundation/client.d.mts +16 -7
  8. package/dist/components/foundation/client.mjs +1 -1
  9. package/dist/components/foundation/client.mjs.map +1 -1
  10. package/dist/components/foundation/modules/command/command.d.mts +50 -10
  11. package/dist/components/foundation/modules/command/command.mjs +1 -1
  12. package/dist/components/foundation/modules/command/command.mjs.map +1 -1
  13. package/dist/components/foundation/modules/command/lifecycle.d.mts +81 -0
  14. package/dist/components/foundation/modules/command/lifecycle.mjs +2 -0
  15. package/dist/components/foundation/modules/command/lifecycle.mjs.map +1 -0
  16. package/dist/components/foundation/modules/observability/observability.d.mts +8 -0
  17. package/dist/components/foundation/modules/observability/observability.mjs +1 -1
  18. package/dist/components/foundation/modules/observability/observability.mjs.map +1 -1
  19. package/dist/components/foundation/operation.d.mts +6 -6
  20. package/dist/components/foundation/operation.mjs.map +1 -1
  21. package/dist/contracts.d.mts +4 -0
  22. package/dist/contracts.mjs +1 -0
  23. package/dist/crud.d.mts +2 -2
  24. package/dist/crud.mjs.map +1 -1
  25. package/dist/effect.d.mts +12 -0
  26. package/dist/effect.mjs +1 -0
  27. package/dist/errors.d.mts +2 -1
  28. package/dist/errors.mjs +1 -1
  29. package/dist/errors.mjs.map +1 -1
  30. package/dist/idempotency.d.mts +2 -0
  31. package/dist/idempotency.mjs +1 -0
  32. package/dist/modules/contracts/contract.d.mts +70 -0
  33. package/dist/modules/contracts/contract.mjs +2 -0
  34. package/dist/modules/contracts/contract.mjs.map +1 -0
  35. package/dist/modules/contracts/convex.d.mts +22 -0
  36. package/dist/modules/contracts/convex.mjs +2 -0
  37. package/dist/modules/contracts/convex.mjs.map +1 -0
  38. package/dist/modules/contracts/errors.d.mts +44 -0
  39. package/dist/modules/contracts/errors.mjs +2 -0
  40. package/dist/modules/contracts/errors.mjs.map +1 -0
  41. package/dist/modules/contracts/exposure.d.mts +105 -0
  42. package/dist/modules/contracts/exposure.mjs +2 -0
  43. package/dist/modules/contracts/exposure.mjs.map +1 -0
  44. package/dist/modules/contracts/idempotency.d.mts +87 -0
  45. package/dist/modules/contracts/idempotency.mjs +2 -0
  46. package/dist/modules/contracts/idempotency.mjs.map +1 -0
  47. package/dist/modules/contracts/standard.d.mts +16 -0
  48. package/dist/modules/contracts/standard.mjs +2 -0
  49. package/dist/modules/contracts/standard.mjs.map +1 -0
  50. package/dist/modules/contracts/workflow.d.mts +167 -0
  51. package/dist/modules/contracts/workflow.mjs +2 -0
  52. package/dist/modules/contracts/workflow.mjs.map +1 -0
  53. package/dist/modules/effect/agent-tools.d.mts +2 -0
  54. package/dist/modules/effect/agent-tools.mjs +1 -0
  55. package/dist/modules/effect/api-runtime.d.mts +38 -0
  56. package/dist/modules/effect/api-runtime.mjs +2 -0
  57. package/dist/modules/effect/api-runtime.mjs.map +1 -0
  58. package/dist/modules/effect/api-zod.d.mts +35 -0
  59. package/dist/modules/effect/api-zod.mjs +2 -0
  60. package/dist/modules/effect/api-zod.mjs.map +1 -0
  61. package/dist/modules/effect/api.d.mts +21 -0
  62. package/dist/modules/effect/api.mjs +2 -0
  63. package/dist/modules/effect/api.mjs.map +1 -0
  64. package/dist/modules/effect/client.d.mts +11 -0
  65. package/dist/modules/effect/client.mjs +1 -0
  66. package/dist/modules/effect/command.d.mts +63 -0
  67. package/dist/modules/effect/command.mjs +2 -0
  68. package/dist/modules/effect/command.mjs.map +1 -0
  69. package/dist/modules/effect/crud.d.mts +785 -0
  70. package/dist/modules/effect/crud.mjs +2 -0
  71. package/dist/modules/effect/crud.mjs.map +1 -0
  72. package/dist/modules/effect/errors.d.mts +8 -0
  73. package/dist/modules/effect/errors.mjs +2 -0
  74. package/dist/modules/effect/errors.mjs.map +1 -0
  75. package/dist/modules/effect/foundation.d.mts +21 -0
  76. package/dist/modules/effect/foundation.mjs +2 -0
  77. package/dist/modules/effect/foundation.mjs.map +1 -0
  78. package/dist/modules/effect/idempotency.d.mts +31 -0
  79. package/dist/modules/effect/idempotency.mjs +2 -0
  80. package/dist/modules/effect/idempotency.mjs.map +1 -0
  81. package/dist/modules/effect/normalize.d.mts +10 -0
  82. package/dist/modules/effect/normalize.mjs +2 -0
  83. package/dist/modules/effect/normalize.mjs.map +1 -0
  84. package/dist/modules/effect/operation.d.mts +141 -0
  85. package/dist/modules/effect/operation.mjs +2 -0
  86. package/dist/modules/effect/operation.mjs.map +1 -0
  87. package/dist/modules/effect/query.d.mts +69 -0
  88. package/dist/modules/effect/query.mjs +2 -0
  89. package/dist/modules/effect/query.mjs.map +1 -0
  90. package/dist/modules/effect/schema.d.mts +33 -0
  91. package/dist/modules/effect/schema.mjs +2 -0
  92. package/dist/modules/effect/schema.mjs.map +1 -0
  93. package/dist/modules/effect/workflow.d.mts +77 -0
  94. package/dist/modules/effect/workflow.mjs +2 -0
  95. package/dist/modules/effect/workflow.mjs.map +1 -0
  96. package/dist/node_modules/@standard-schema/spec/dist/index.d.mts +79 -0
  97. package/dist/workflow.d.mts +2 -0
  98. package/dist/workflow.mjs +1 -0
  99. package/docs/architecture.md +3 -3
  100. package/docs/auth.md +71 -0
  101. package/docs/command-context.md +150 -0
  102. package/docs/commands-legacy.md +511 -0
  103. package/docs/commands.md +143 -363
  104. package/docs/conventions.md +15 -5
  105. package/docs/crud.md +4 -0
  106. package/docs/domain-operations.md +615 -0
  107. package/docs/effect.md +395 -0
  108. package/docs/llms.md +69 -45
  109. package/docs/migration.md +1 -1
  110. package/docs/upgrading.md +9 -0
  111. package/package.json +33 -1
  112. package/scripts/convex-smoke-env.mjs +33 -0
  113. package/scripts/smoke-packed-components.mjs +9 -2
  114. package/scripts/smoke-packed-effect.mjs +439 -0
  115. package/scripts/test-packed-effect.mjs +460 -0
  116. package/scripts/test-packed-ids.mjs +1 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cvx-kit",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "Reusable Convex application kit: foundation kernels, approvals component, zod table boundaries, WorkOS-backed auth function constructors, and audited command protocol.",
5
5
  "keywords": [
6
6
  "component",
@@ -30,6 +30,22 @@
30
30
  "types": "./dist/zod-table.d.mts",
31
31
  "import": "./dist/zod-table.mjs"
32
32
  },
33
+ "./effect": {
34
+ "types": "./dist/effect.d.mts",
35
+ "import": "./dist/effect.mjs"
36
+ },
37
+ "./contracts": {
38
+ "types": "./dist/contracts.d.mts",
39
+ "import": "./dist/contracts.mjs"
40
+ },
41
+ "./idempotency": {
42
+ "types": "./dist/idempotency.d.mts",
43
+ "import": "./dist/idempotency.mjs"
44
+ },
45
+ "./workflow": {
46
+ "types": "./dist/workflow.d.mts",
47
+ "import": "./dist/workflow.mjs"
48
+ },
33
49
  "./auth": {
34
50
  "types": "./dist/auth.d.mts",
35
51
  "import": "./dist/auth.mjs"
@@ -116,6 +132,8 @@
116
132
  "test:package": "bun run build && vp test run test/package-contents.test.mjs",
117
133
  "test:package:ids": "bun run build && node scripts/test-packed-ids.mjs",
118
134
  "test:package:oxlint": "bun run build && node scripts/test-packed-oxlint.mjs",
135
+ "test:package:effect": "bun run build && node scripts/test-packed-effect.mjs",
136
+ "test:package:smoke:effect": "bun run build && node scripts/smoke-packed-effect.mjs",
119
137
  "test:package:smoke:bun": "bun scripts/smoke-packed-components.mjs bun",
120
138
  "test:package:smoke:npm": "node scripts/smoke-packed-components.mjs npm",
121
139
  "docs:sync": "rm -f docs/*.md && cp src/docs/*.md docs/",
@@ -137,6 +155,7 @@
137
155
  "convex": "1.45.0",
138
156
  "convex-helpers": "0.1.124",
139
157
  "convex-test": "^0.0.56",
158
+ "effect": "4.0.1",
140
159
  "oxlint": "1.79.0",
141
160
  "publint": "^0.3.24",
142
161
  "typescript": "7.0.2",
@@ -146,6 +165,19 @@
146
165
  "peerDependencies": {
147
166
  "convex": "^1.45.0",
148
167
  "convex-helpers": ">=0.1.123 <0.2.0",
168
+ "effect": ">=4.0.1 <5",
149
169
  "zod": "^4.5.4"
170
+ },
171
+ "peerDependenciesMeta": {
172
+ "effect": {
173
+ "optional": true
174
+ }
175
+ },
176
+ "devEngines": {
177
+ "packageManager": {
178
+ "name": "bun",
179
+ "version": "1.4.2",
180
+ "onFail": "download"
181
+ }
150
182
  }
151
183
  }
@@ -0,0 +1,33 @@
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
+ }
@@ -2,6 +2,11 @@ import { execFileSync } from 'node:child_process'
2
2
  import { copyFileSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
3
3
  import { tmpdir } from 'node:os'
4
4
  import { join } from 'node:path'
5
+ import {
6
+ assertIsolatedConvexFixture,
7
+ assertLocalConvexState,
8
+ localConvexEnvironment,
9
+ } from './convex-smoke-env.mjs'
5
10
 
6
11
  const installer = process.argv[2] ?? 'bun'
7
12
  if (installer !== 'bun' && installer !== 'npm') {
@@ -20,7 +25,7 @@ function run(command, args, cwd = fixture) {
20
25
  cwd,
21
26
  encoding: 'utf8',
22
27
  stdio: ['ignore', 'pipe', 'pipe'],
23
- env: { ...process.env, CONVEX_AGENT_MODE: 'anonymous' },
28
+ env: localConvexEnvironment(),
24
29
  })
25
30
  }
26
31
 
@@ -28,7 +33,7 @@ function runStreaming(command, args, cwd = fixture) {
28
33
  execFileSync(command, args, {
29
34
  cwd,
30
35
  stdio: 'inherit',
31
- env: { ...process.env, CONVEX_AGENT_MODE: 'anonymous' },
36
+ env: localConvexEnvironment(),
32
37
  })
33
38
  }
34
39
 
@@ -247,7 +252,9 @@ export const status = query({
247
252
  runStreaming('npx', ['vp', 'test', 'run', 'packed-test-helper.test.ts']),
248
253
  )
249
254
 
255
+ assertIsolatedConvexFixture(fixture)
250
256
  step('deploy Convex fixture', () => runConvex('dev', '--once', '--typecheck=disable'))
257
+ assertLocalConvexState(fixture)
251
258
  const health = JSON.parse(runConvex('run', 'smoke:health'))
252
259
  if (health.status !== 'ready' || health.schemaVersion !== 1) {
253
260
  throw new Error(`Unexpected approvals health response: ${JSON.stringify(health)}`)
@@ -0,0 +1,439 @@
1
+ import assert from 'node:assert/strict'
2
+ import { execFileSync, spawn } from 'node:child_process'
3
+ import {
4
+ cpSync,
5
+ mkdirSync,
6
+ mkdtempSync,
7
+ readFileSync,
8
+ readdirSync,
9
+ rmSync,
10
+ writeFileSync,
11
+ } from 'node:fs'
12
+ import { createServer } from 'node:net'
13
+ import { tmpdir } from 'node:os'
14
+ import { join } from 'node:path'
15
+ import {
16
+ assertIsolatedConvexFixture,
17
+ assertLocalConvexState,
18
+ localConvexEnvironment,
19
+ } from './convex-smoke-env.mjs'
20
+
21
+ const root = join(import.meta.dirname, '..')
22
+ const manifest = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'))
23
+ const installer = process.argv[2] ?? 'bun'
24
+ if (!['bun', 'npm'].includes(installer)) throw new Error('Usage: smoke-packed-effect.mjs [bun|npm]')
25
+ const temporaryRoot = mkdtempSync(join(tmpdir(), 'cvx-kit-effect-runtime-'))
26
+ const fixture = join(temporaryRoot, 'fixture')
27
+ function run(command, args, cwd = fixture) {
28
+ try {
29
+ return execFileSync(command, args, {
30
+ cwd,
31
+ encoding: 'utf8',
32
+ stdio: ['ignore', 'pipe', 'pipe'],
33
+ env: localConvexEnvironment(),
34
+ })
35
+ } catch (error) {
36
+ if (args[0] === 'install')
37
+ throw new Error(
38
+ `Isolated ${command} install failed with exit ${error.status ?? 'unknown'}; package-manager output suppressed`,
39
+ )
40
+ throw error
41
+ }
42
+ }
43
+ function convex(...args) {
44
+ return run('node', [join(fixture, 'node_modules/convex/bin/main.js'), ...args])
45
+ }
46
+ async function verifyNativeFixture(script, label, port, environment = {}) {
47
+ const child = spawn(
48
+ 'node',
49
+ [
50
+ join(fixture, 'node_modules/convex/bin/main.js'),
51
+ 'dev',
52
+ '--once',
53
+ '--typecheck=disable',
54
+ '--start',
55
+ `node ${script} ${port}`,
56
+ ],
57
+ {
58
+ cwd: fixture,
59
+ env: { ...localConvexEnvironment(), ...environment },
60
+ stdio: ['pipe', 'pipe', 'pipe'],
61
+ },
62
+ )
63
+ let output = ''
64
+ let diagnostics = ''
65
+ child.stdout.setEncoding('utf8').on('data', (chunk) => {
66
+ output += chunk
67
+ })
68
+ child.stderr.setEncoding('utf8').on('data', (chunk) => {
69
+ diagnostics += chunk
70
+ })
71
+ const deadline = setTimeout(() => child.kill('SIGINT'), 90000)
72
+ try {
73
+ const code = await new Promise((resolve, reject) => {
74
+ child.once('error', reject)
75
+ child.once('close', resolve)
76
+ })
77
+ process.stdout.write(output)
78
+ process.stderr.write(diagnostics)
79
+ assert.equal(code, 0, `Native ${label} command must complete successfully`)
80
+ const marker = environment.WORKFLOW_NATIVE_PHASE
81
+ ? `U6 real native deployment phase passed: ${environment.WORKFLOW_NATIVE_PHASE}`
82
+ : `Native ${label} passed:`
83
+ assert.ok(output.includes(marker))
84
+ } finally {
85
+ clearTimeout(deadline)
86
+ child.stdin.end()
87
+ }
88
+ }
89
+ async function verifyWorkflowDeployments(port) {
90
+ const sourcePath = join(fixture, 'convex/workflowNative.ts')
91
+ const source = readFileSync(sourcePath, 'utf8')
92
+ assert.ok(source.includes("export const deployedBindingVersion = '1'"))
93
+ assert.ok(source.includes("export const deployedArgumentSuffix = ''"))
94
+ const state = join(temporaryRoot, 'workflow-deployment-state.json')
95
+ const phase = (value) =>
96
+ verifyNativeFixture('workflow-proof.mjs', 'workflow proof', port, {
97
+ WORKFLOW_NATIVE_PHASE: value,
98
+ WORKFLOW_NATIVE_PHASE_STATE: state,
99
+ })
100
+ try {
101
+ await phase('prepare')
102
+ writeFileSync(
103
+ sourcePath,
104
+ source.replace(
105
+ "export const deployedBindingVersion = '1'",
106
+ "export const deployedBindingVersion = '2'",
107
+ ),
108
+ )
109
+ await phase('compatible')
110
+ writeFileSync(
111
+ sourcePath,
112
+ source.replace(
113
+ "export const deployedArgumentSuffix = ''",
114
+ "export const deployedArgumentSuffix = '-changed'",
115
+ ),
116
+ )
117
+ await phase('changed-args')
118
+ } finally {
119
+ writeFileSync(sourcePath, source)
120
+ convex('dev', '--once', '--typecheck=disable')
121
+ }
122
+ }
123
+ function invoke(name, args = {}, identity) {
124
+ const output = convex(
125
+ 'run',
126
+ name.includes(':') ? name : `functions:${name}`,
127
+ JSON.stringify(args),
128
+ ...(identity ? ['--identity', JSON.stringify(identity)] : []),
129
+ )
130
+ // Convex 1.45's run command emits no output for a successful null result.
131
+ return output.trim() === '' ? null : JSON.parse(output)
132
+ }
133
+ function rejection(name, args, expected, identity) {
134
+ assert.throws(
135
+ () => invoke(name, args, identity),
136
+ (error) => {
137
+ const message = String(error.stderr)
138
+ assert.match(message, expected)
139
+ assert.doesNotMatch(message, /PROJECTION_MUST_NOT_RUN/)
140
+ return true
141
+ },
142
+ )
143
+ }
144
+ async function freePort() {
145
+ const server = createServer()
146
+ await new Promise((resolve, reject) => {
147
+ server.once('error', reject)
148
+ server.listen(0, '127.0.0.1', resolve)
149
+ })
150
+ const port = server.address().port
151
+ await new Promise((resolve) => server.close(resolve))
152
+ return port
153
+ }
154
+ try {
155
+ console.log('Effect runtime smoke: pack current built artifact (no build)')
156
+ run('bun', ['pm', 'pack', '--destination', temporaryRoot, '--ignore-scripts'], root)
157
+ mkdirSync(fixture)
158
+ cpSync(join(root, 'test/fixture-effect/convex'), join(fixture, 'convex'), { recursive: true })
159
+ // Generate from scratch so removed generated files cannot survive the fixture copy.
160
+ rmSync(join(fixture, 'convex/_generated'), { recursive: true, force: true })
161
+ writeFileSync(
162
+ join(fixture, 'package.json'),
163
+ JSON.stringify(
164
+ {
165
+ private: true,
166
+ type: 'module',
167
+ dependencies: {
168
+ 'cvx-kit': `file:${join(temporaryRoot, `cvx-kit-${manifest.version}.tgz`)}`,
169
+ '@convex-dev/workflow': manifest.dependencies['@convex-dev/workflow'],
170
+ convex: manifest.devDependencies.convex,
171
+ effect: manifest.devDependencies.effect,
172
+ zod: manifest.devDependencies.zod,
173
+ 'convex-helpers': manifest.devDependencies['convex-helpers'],
174
+ },
175
+ devDependencies: {
176
+ typescript: manifest.devDependencies.typescript,
177
+ '@types/node': manifest.devDependencies['@types/node'],
178
+ },
179
+ },
180
+ null,
181
+ 2,
182
+ ),
183
+ )
184
+ writeFileSync(
185
+ join(fixture, 'tsconfig.json'),
186
+ JSON.stringify(
187
+ {
188
+ compilerOptions: {
189
+ target: 'ES2025',
190
+ lib: ['ES2025', 'DOM'],
191
+ module: 'ESNext',
192
+ moduleResolution: 'Bundler',
193
+ strict: true,
194
+ noEmit: true,
195
+ skipLibCheck: false,
196
+ types: ['node'],
197
+ },
198
+ include: ['convex', 'typecheck.ts'],
199
+ },
200
+ null,
201
+ 2,
202
+ ),
203
+ )
204
+ cpSync(join(root, 'test/fixture-effect/typecheck.ts'), join(fixture, 'typecheck.ts'))
205
+ cpSync(join(root, 'test/fixture-effect/pagination.mjs'), join(fixture, 'pagination.mjs'))
206
+ cpSync(join(root, 'test/fixture-effect/idempotency.mjs'), join(fixture, 'idempotency.mjs'))
207
+ cpSync(join(root, 'test/fixture-effect/async-domain.mjs'), join(fixture, 'async-domain.mjs'))
208
+ cpSync(
209
+ join(root, 'test/fixture-effect/action-validation.mjs'),
210
+ join(fixture, 'action-validation.mjs'),
211
+ )
212
+ cpSync(
213
+ join(root, 'test/fixture-effect/integrated-rename.mjs'),
214
+ join(fixture, 'integrated-rename.mjs'),
215
+ )
216
+ cpSync(
217
+ join(root, 'test/fixture-effect/deployment-versions.mjs'),
218
+ join(fixture, 'deployment-versions.mjs'),
219
+ )
220
+ cpSync(join(root, 'scripts/convex-smoke-env.mjs'), join(fixture, 'convex-smoke-env.mjs'))
221
+ cpSync(join(root, 'test/fixture-effect/workflow-proof.mjs'), join(fixture, 'workflow-proof.mjs'))
222
+ cpSync(
223
+ join(root, 'test/fixture-effect/operation-tools.mjs'),
224
+ join(fixture, 'operation-tools.mjs'),
225
+ )
226
+ console.log(`Effect runtime smoke: isolated ${installer} install`)
227
+ run(installer, ['install', '--ignore-scripts'])
228
+ assertIsolatedConvexFixture(fixture)
229
+ const cloudPort = await freePort()
230
+ let sitePort = await freePort()
231
+ while (sitePort === cloudPort) sitePort = await freePort()
232
+ console.log('Effect runtime smoke: deploy isolated local Convex fixture')
233
+ convex(
234
+ 'dev',
235
+ '--once',
236
+ '--typecheck=disable',
237
+ '--local-cloud-port',
238
+ String(cloudPort),
239
+ '--local-site-port',
240
+ String(sitePort),
241
+ )
242
+ assertLocalConvexState(fixture)
243
+ convex('codegen', '--typecheck=disable')
244
+ const generated = join(fixture, 'convex/_generated')
245
+ const committed = join(root, 'test/fixture-effect/convex/_generated')
246
+ const regenerateMessage = 'Generated contracts differ; regenerate the Effect fixture contracts'
247
+ assert.deepEqual(readdirSync(generated).sort(), readdirSync(committed).sort(), regenerateMessage)
248
+ for (const file of readdirSync(generated)) {
249
+ assert.equal(
250
+ readFileSync(join(generated, file), 'utf8'),
251
+ readFileSync(join(committed, file), 'utf8'),
252
+ `${regenerateMessage}: ${file}`,
253
+ )
254
+ }
255
+ console.log('Effect runtime smoke: strict generated contracts and packed declarations')
256
+ run('node', [join(fixture, 'node_modules/typescript/bin/tsc'), '--noEmit'])
257
+ const localConfig = JSON.parse(
258
+ readFileSync(join(fixture, '.convex/local/default/config.json'), 'utf8'),
259
+ )
260
+ console.log(
261
+ `Runtime versions: Convex CLI ${manifest.devDependencies.convex}; Effect ${manifest.devDependencies.effect}; backend ${localConfig.backendVersion ?? 'not recorded by CLI'}`,
262
+ )
263
+ // Keep the local backend alive for the subscription; one-off CLI calls stop it on exit.
264
+ await verifyNativeFixture('pagination.mjs', 'pagination', cloudPort)
265
+ await verifyNativeFixture('idempotency.mjs', 'idempotency', cloudPort)
266
+ const versionProof = run('node', ['deployment-versions.mjs', String(cloudPort)])
267
+ assert.ok(versionProof.includes('Native deployment versions passed:'))
268
+ process.stdout.write(versionProof)
269
+ await verifyNativeFixture('workflow-proof.mjs', 'workflow proof', cloudPort)
270
+ await verifyWorkflowDeployments(cloudPort)
271
+ await verifyNativeFixture('operation-tools.mjs', 'operation tools', cloudPort)
272
+ await verifyNativeFixture('async-domain.mjs', 'async domain', cloudPort)
273
+ await verifyNativeFixture('action-validation.mjs', 'action validation', cloudPort)
274
+ await verifyNativeFixture('integrated-rename.mjs', 'integrated rename', cloudPort)
275
+ assert.equal(invoke('save', { key: 'success', mode: 'success' }), 'saved')
276
+ assert.deepEqual(invoke('read', { key: 'success' }), ['domain', 'audit', 'completion'])
277
+ assert.deepEqual(invoke('composed', { key: 'success' }), {
278
+ count: 6,
279
+ events: ['acquire', 'handler', 'release'],
280
+ })
281
+ rejection('save', { key: 'invalid-args', mode: 'unknown' }, /ArgumentValidationError/)
282
+ assert.deepEqual(invoke('read', { key: 'invalid-args' }), [])
283
+ rejection('invalidReturn', { key: 'invalid-return' }, /ReturnsValidationError/)
284
+ assert.deepEqual(invoke('read', { key: 'invalid-return' }), [])
285
+ rejection('invalidCustomReturn', { key: 'invalid-custom-return' }, /FIXTURE_CUSTOM_RETURN/)
286
+ assert.deepEqual(invoke('read', { key: 'invalid-custom-return' }), [])
287
+ for (const mode of ['domain', 'audit', 'completion']) {
288
+ rejection('save', { key: mode, mode }, new RegExp(`FIXTURE_${mode}`))
289
+ assert.deepEqual(invoke('read', { key: mode }), [])
290
+ }
291
+ rejection('cleanupFailure', { composite: false }, /FIXTURE_CLEANUP/)
292
+ assert.deepEqual(invoke('read', { key: 'cleanup' }), [])
293
+ rejection('cleanupFailure', { composite: true }, /Effect API execution failed/)
294
+ assert.deepEqual(invoke('read', { key: 'cleanup' }), [])
295
+ assert.deepEqual(invoke('read', { key: 'provider-initialization' }), [])
296
+ for (let invocation = 0; invocation < 2; invocation++) {
297
+ assert.throws(
298
+ () => invoke('providerInitializationFailure'),
299
+ (error) => {
300
+ const message = String(error.stderr)
301
+ assert.match(message, /FIXTURE_PROVIDER_INITIALIZATION/)
302
+ assert.match(
303
+ message,
304
+ /"events"\s*:\s*\[\s*"acquire"\s*,\s*"initialize"\s*,\s*"release"\s*\]/,
305
+ )
306
+ assert.doesNotMatch(message, /"handler"|FIXTURE_PROVIDER_UNEXPECTED/)
307
+ return true
308
+ },
309
+ )
310
+ assert.deepEqual(
311
+ invoke('read', { key: 'provider-initialization' }),
312
+ [],
313
+ 'Provider acquisition and awaited release writes must roll back after initialization fails',
314
+ )
315
+ }
316
+ assert.deepEqual(invoke('actions:orchestrate', { key: 'action' }), {
317
+ body: 'fixture-http-ok',
318
+ stages: ['domain', 'audit', 'completion'],
319
+ rejected: true,
320
+ })
321
+ assert.deepEqual(
322
+ invoke('actions:declaredFailureRoundTrip', { key: 'declared-round-trip', composite: false }),
323
+ {
324
+ kind: 'declared',
325
+ code: 'DENIED',
326
+ key: 'declared-round-trip',
327
+ },
328
+ )
329
+ assert.deepEqual(invoke('read', { key: 'declared-round-trip' }), [])
330
+ assert.deepEqual(
331
+ invoke('actions:declaredFailureRoundTrip', { key: 'composite-round-trip', composite: true }),
332
+ { kind: 'unknown' },
333
+ )
334
+ assert.deepEqual(invoke('read', { key: 'composite-round-trip' }), [])
335
+ const crudOwner = { subject: 'crud-owner', org_id: 'tenant-a', role: 'owner' }
336
+ const otherId = invoke('crud:createOther', {}, crudOwner)
337
+ const initialOtherState = invoke('crud:otherState', { id: otherId })
338
+ assert.deepEqual(initialOtherState, { title: 'other-table', audits: [] })
339
+ for (const [operation, input] of [
340
+ ['crud:getFromString', { id: otherId }],
341
+ ['crud:updateFromString', { id: otherId, data: { title: 'wrong-table-write' } }],
342
+ ['crud:archiveFromString', { id: otherId }],
343
+ ]) {
344
+ rejection(operation, input, /Invalid ID for table "crudNotes"/, crudOwner)
345
+ assert.deepEqual(invoke('crud:otherState', { id: otherId }), initialOtherState)
346
+ }
347
+ const created = invoke('crud:create', { title: 'native-crud' }, crudOwner)
348
+ assert.match(created.id, /^.+$/)
349
+ assert.deepEqual(invoke('crud:get', { id: created.id }, crudOwner), { title: 'native-crud' })
350
+ const initialCrudState = invoke('crud:state', { id: created.id })
351
+ assert.deepEqual(initialCrudState, {
352
+ note: {
353
+ title: 'native-crud',
354
+ tenant: 'tenant-a',
355
+ owner: 'crud-owner',
356
+ secret: 'server-private',
357
+ },
358
+ history: ['insert'],
359
+ audits: [{ operation: 'crudNotes.create', actor: 'crud-owner' }],
360
+ })
361
+ rejection('crud:get', { id: created.id }, /UNAUTHENTICATED|authenticated/i)
362
+ rejection(
363
+ 'crud:update',
364
+ { id: created.id, data: { title: 'viewer-write' } },
365
+ /access|allow|permission/i,
366
+ { ...crudOwner, subject: 'crud-viewer', role: 'viewer' },
367
+ )
368
+ rejection(
369
+ 'crud:update',
370
+ { id: created.id, data: { tenant: 'tenant-b', secret: 'caller-secret' } },
371
+ /validation|invalid|unrecognized|extra/i,
372
+ crudOwner,
373
+ )
374
+ const foreignOwner = { ...crudOwner, subject: 'foreign-owner', org_id: 'tenant-b' }
375
+ assert.equal(invoke('crud:get', { id: created.id }, foreignOwner), null)
376
+ assert.deepEqual(invoke('crud:list', { numItems: 2, cursor: null }, foreignOwner).page, [])
377
+ assert.deepEqual(invoke('crud:list', { numItems: 2, cursor: null }, crudOwner).page, [
378
+ { title: 'native-crud' },
379
+ ])
380
+ rejection('crud:list', { numItems: 3, cursor: null }, /validation|invalid|too big/i, crudOwner)
381
+ rejection(
382
+ 'crud:update',
383
+ { id: created.id, data: { title: 'foreign' } },
384
+ /access|exist/i,
385
+ foreignOwner,
386
+ )
387
+ rejection(
388
+ 'crud:update',
389
+ { id: created.id, data: { title: 'audit-rollback' }, failAudit: true },
390
+ /CRUD_AUDIT_FAILURE/,
391
+ crudOwner,
392
+ )
393
+ rejection('crud:invalidOutput', { id: created.id }, /invalid|expected|validation/i, crudOwner)
394
+ assert.deepEqual(
395
+ invoke('crud:state', { id: created.id }),
396
+ initialCrudState,
397
+ 'RLS, audit and native output failures must leave business, trigger and audit rows unchanged',
398
+ )
399
+ assert.deepEqual(
400
+ invoke('crud:update', { id: created.id, data: { title: 'native-updated' } }, crudOwner),
401
+ { ok: true },
402
+ )
403
+ assert.deepEqual(invoke('crud:get', { id: created.id }, crudOwner), { title: 'native-updated' })
404
+ const updatedCrudState = invoke('crud:state', { id: created.id })
405
+ assert.deepEqual(updatedCrudState.note, { ...initialCrudState.note, title: 'native-updated' })
406
+ assert.deepEqual(updatedCrudState.history, ['insert', 'update'])
407
+ assert.deepEqual(updatedCrudState.audits, [
408
+ ...initialCrudState.audits,
409
+ { operation: 'crudNotes.update', actor: 'crud-owner' },
410
+ ])
411
+ assert.deepEqual(invoke('crud:update', { id: created.id, data: {} }, crudOwner), { ok: true })
412
+ for (let attempt = 0; attempt < 2; attempt++) {
413
+ assert.deepEqual(invoke('crud:archive', { id: created.id }, crudOwner), { ok: true })
414
+ }
415
+ const archivedCrudState = invoke('crud:state', { id: created.id })
416
+ assert.equal(archivedCrudState.note.title, 'native-updated')
417
+ assert.ok(Number.isFinite(archivedCrudState.note.archivedAt))
418
+ assert.deepEqual(archivedCrudState.history, ['insert', 'update', 'update', 'update', 'update'])
419
+ assert.deepEqual(
420
+ archivedCrudState.audits.map((entry) => entry.operation),
421
+ [
422
+ 'crudNotes.create',
423
+ 'crudNotes.update',
424
+ 'crudNotes.update',
425
+ 'crudNotes.archive',
426
+ 'crudNotes.archive',
427
+ ],
428
+ )
429
+ assert.ok(archivedCrudState.audits.every((entry) => entry.actor === 'crud-owner'))
430
+ console.log(
431
+ 'Actual local runtime passed: async query ordinary/gen/fn composition, spans, awaited scoped release, domain/audit/completion rollback, declared ConvexError.data round-trip, composite safe unknown round-trip, projected rejection, cleanup/composite rejection, HTTP action and separate transaction commits, secured CRUD tenant projection/pagination, nonempty/empty update persistence, repeated archive, and business/trigger/audit rollback',
432
+ )
433
+ } catch (error) {
434
+ if (error.stdout) process.stderr.write(String(error.stdout))
435
+ if (error.stderr) process.stderr.write(String(error.stderr))
436
+ throw error
437
+ } finally {
438
+ rmSync(temporaryRoot, { recursive: true, force: true })
439
+ }