cvx-kit 0.1.5 → 0.1.7
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 +41 -0
- package/README.md +21 -3
- package/dist/agent-tools.d.mts +2 -1
- package/dist/agent-tools.mjs +1 -1
- package/dist/agent-tools.mjs.map +1 -1
- package/dist/components/approvals/audit.mjs +1 -1
- package/dist/components/foundation/client.d.mts +8 -7
- package/dist/components/foundation/client.mjs +1 -1
- package/dist/components/foundation/client.mjs.map +1 -1
- package/dist/components/foundation/modules/command/command.d.mts +50 -10
- package/dist/components/foundation/modules/command/command.mjs +1 -1
- package/dist/components/foundation/modules/command/command.mjs.map +1 -1
- package/dist/components/foundation/modules/command/lifecycle.d.mts +81 -0
- package/dist/components/foundation/modules/command/lifecycle.mjs +2 -0
- package/dist/components/foundation/modules/command/lifecycle.mjs.map +1 -0
- package/dist/components/foundation/modules/observability/observability.d.mts +8 -0
- package/dist/components/foundation/modules/observability/observability.mjs +1 -1
- package/dist/components/foundation/modules/observability/observability.mjs.map +1 -1
- package/dist/components/foundation/operation.d.mts +6 -6
- package/dist/components/foundation/operation.mjs.map +1 -1
- package/dist/contracts.d.mts +4 -0
- package/dist/contracts.mjs +1 -0
- package/dist/crud.d.mts +2 -2
- package/dist/crud.mjs.map +1 -1
- package/dist/effect.d.mts +12 -0
- package/dist/effect.mjs +1 -0
- package/dist/errors.d.mts +2 -1
- package/dist/errors.mjs +1 -1
- package/dist/errors.mjs.map +1 -1
- package/dist/idempotency.d.mts +2 -0
- package/dist/idempotency.mjs +1 -0
- package/dist/modules/contracts/contract.d.mts +70 -0
- package/dist/modules/contracts/contract.mjs +2 -0
- package/dist/modules/contracts/contract.mjs.map +1 -0
- package/dist/modules/contracts/convex.d.mts +22 -0
- package/dist/modules/contracts/convex.mjs +2 -0
- package/dist/modules/contracts/convex.mjs.map +1 -0
- package/dist/modules/contracts/errors.d.mts +44 -0
- package/dist/modules/contracts/errors.mjs +2 -0
- package/dist/modules/contracts/errors.mjs.map +1 -0
- package/dist/modules/contracts/exposure.d.mts +105 -0
- package/dist/modules/contracts/exposure.mjs +2 -0
- package/dist/modules/contracts/exposure.mjs.map +1 -0
- package/dist/modules/contracts/idempotency.d.mts +87 -0
- package/dist/modules/contracts/idempotency.mjs +2 -0
- package/dist/modules/contracts/idempotency.mjs.map +1 -0
- package/dist/modules/contracts/standard.d.mts +16 -0
- package/dist/modules/contracts/standard.mjs +2 -0
- package/dist/modules/contracts/standard.mjs.map +1 -0
- package/dist/modules/contracts/workflow.d.mts +167 -0
- package/dist/modules/contracts/workflow.mjs +2 -0
- package/dist/modules/contracts/workflow.mjs.map +1 -0
- package/dist/modules/effect/agent-tools.d.mts +2 -0
- package/dist/modules/effect/agent-tools.mjs +1 -0
- package/dist/modules/effect/api-runtime.d.mts +28 -0
- package/dist/modules/effect/api-runtime.mjs +2 -0
- package/dist/modules/effect/api-runtime.mjs.map +1 -0
- package/dist/modules/effect/api-zod.d.mts +35 -0
- package/dist/modules/effect/api-zod.mjs +2 -0
- package/dist/modules/effect/api-zod.mjs.map +1 -0
- package/dist/modules/effect/api.d.mts +21 -0
- package/dist/modules/effect/api.mjs +2 -0
- package/dist/modules/effect/api.mjs.map +1 -0
- package/dist/modules/effect/client.d.mts +11 -0
- package/dist/modules/effect/client.mjs +1 -0
- package/dist/modules/effect/command.d.mts +59 -0
- package/dist/modules/effect/command.mjs +2 -0
- package/dist/modules/effect/command.mjs.map +1 -0
- package/dist/modules/effect/crud.d.mts +785 -0
- package/dist/modules/effect/crud.mjs +2 -0
- package/dist/modules/effect/crud.mjs.map +1 -0
- package/dist/modules/effect/errors.d.mts +8 -0
- package/dist/modules/effect/errors.mjs +2 -0
- package/dist/modules/effect/errors.mjs.map +1 -0
- package/dist/modules/effect/foundation.d.mts +21 -0
- package/dist/modules/effect/foundation.mjs +2 -0
- package/dist/modules/effect/foundation.mjs.map +1 -0
- package/dist/modules/effect/idempotency.d.mts +31 -0
- package/dist/modules/effect/idempotency.mjs +2 -0
- package/dist/modules/effect/idempotency.mjs.map +1 -0
- package/dist/modules/effect/normalize.d.mts +10 -0
- package/dist/modules/effect/normalize.mjs +2 -0
- package/dist/modules/effect/normalize.mjs.map +1 -0
- package/dist/modules/effect/operation.d.mts +141 -0
- package/dist/modules/effect/operation.mjs +2 -0
- package/dist/modules/effect/operation.mjs.map +1 -0
- package/dist/modules/effect/query.d.mts +65 -0
- package/dist/modules/effect/query.mjs +2 -0
- package/dist/modules/effect/query.mjs.map +1 -0
- package/dist/modules/effect/schema.d.mts +33 -0
- package/dist/modules/effect/schema.mjs +2 -0
- package/dist/modules/effect/schema.mjs.map +1 -0
- package/dist/modules/effect/workflow.d.mts +77 -0
- package/dist/modules/effect/workflow.mjs +2 -0
- package/dist/modules/effect/workflow.mjs.map +1 -0
- package/dist/node_modules/@standard-schema/spec/dist/index.d.mts +79 -0
- package/dist/oxlint/architecture.d.mts +20 -0
- package/dist/oxlint/architecture.mjs +2 -0
- package/dist/oxlint/architecture.mjs.map +1 -0
- package/dist/oxlint/ast.d.mts +33 -0
- package/dist/oxlint/ast.mjs +2 -0
- package/dist/oxlint/ast.mjs.map +1 -0
- package/dist/oxlint/ownership.d.mts +62 -0
- package/dist/oxlint/ownership.mjs +2 -0
- package/dist/oxlint/ownership.mjs.map +1 -0
- package/dist/oxlint.d.mts +5 -2
- package/dist/oxlint.mjs +1 -1
- package/dist/oxlint.mjs.map +1 -1
- package/dist/workflow.d.mts +2 -0
- package/dist/workflow.mjs +1 -0
- package/docs/auth.md +65 -0
- package/docs/commands.md +118 -0
- package/docs/conventions.md +49 -21
- package/docs/domain-operations.md +615 -0
- package/docs/effect.md +384 -0
- package/docs/oxlint.md +74 -26
- package/docs/upgrading.md +13 -0
- package/package.json +37 -3
- package/scripts/convex-smoke-env.mjs +33 -0
- package/scripts/smoke-packed-components.mjs +9 -2
- package/scripts/smoke-packed-effect.mjs +439 -0
- package/scripts/test-packed-effect.mjs +457 -0
- package/scripts/test-packed-ids.mjs +1 -0
- package/scripts/test-packed-oxlint.mjs +24 -2
|
@@ -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:
|
|
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:
|
|
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
|
+
}
|