Repository navigation
Fix production execute failure: __name is not defined in generated modules #630
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,80 @@ | ||
| import { expect, test, vi } from 'vitest' | ||
|
|
||
| const createExecuteExecutorMock = vi.hoisted(() => vi.fn()) | ||
|
|
||
| vi.mock('cloudflare:workers', () => ({ | ||
| exports: { | ||
| KodyFetchGateway: () => ({}), | ||
| }, | ||
| })) | ||
|
|
||
| vi.mock('#mcp/executor.ts', () => ({ | ||
| createExecuteExecutor: (...args: Array<unknown>) => | ||
| createExecuteExecutorMock(...args), | ||
| })) | ||
|
|
||
| const { handleExecuteSmokeRequest, runExecuteSmokeCheck } = | ||
| await import('./execute-maintenance.ts') | ||
|
|
||
| test('runExecuteSmokeCheck executes trivial code through the dynamic worker path', async () => { | ||
| const execute = vi.fn(async () => ({ result: 42, logs: [] })) | ||
| createExecuteExecutorMock.mockReturnValue({ execute }) | ||
|
|
||
| const env = { | ||
| APP_BASE_URL: 'https://example.com', | ||
| LOADER: {}, | ||
| } as Env | ||
|
|
||
| await expect(runExecuteSmokeCheck(env)).resolves.toEqual({ result: 42 }) | ||
| expect(createExecuteExecutorMock).toHaveBeenCalledWith( | ||
| expect.objectContaining({ | ||
| timeoutMs: 10_000, | ||
| gatewayProps: { | ||
| baseUrl: 'https://example.com', | ||
| userId: null, | ||
| storageContext: null, | ||
| }, | ||
| }), | ||
| ) | ||
| expect(execute).toHaveBeenCalledWith('async () => 42', [ | ||
| { | ||
| name: 'kody', | ||
| fns: {}, | ||
| kodyRemoteConnectors: [], | ||
| }, | ||
| ]) | ||
| }) | ||
|
|
||
| test('handleExecuteSmokeRequest enforces auth and reports smoke results', async () => { | ||
| const execute = vi.fn(async () => ({ result: 42, logs: [] })) | ||
| createExecuteExecutorMock.mockReturnValue({ execute }) | ||
|
|
||
| const env = { | ||
| CAPABILITY_REINDEX_SECRET: 'secret', | ||
| APP_BASE_URL: 'https://example.com', | ||
| LOADER: {}, | ||
| } as Env | ||
|
|
||
| const unauthorized = await handleExecuteSmokeRequest( | ||
| new Request('https://example.com/__maintenance/execute-smoke', { | ||
| method: 'POST', | ||
| }), | ||
| env, | ||
| ) | ||
| expect(unauthorized.status).toBe(401) | ||
|
|
||
| const success = await handleExecuteSmokeRequest( | ||
| new Request('https://example.com/__maintenance/execute-smoke', { | ||
| method: 'POST', | ||
| headers: { | ||
| Authorization: 'Bearer secret', | ||
| }, | ||
| }), | ||
| env, | ||
| ) | ||
| expect(success.status).toBe(200) | ||
| await expect(success.json()).resolves.toEqual({ | ||
| ok: true, | ||
| result: 42, | ||
| }) | ||
| }) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| import { exports as workerExports } from 'cloudflare:workers' | ||
| import { createExecuteExecutor } from '#mcp/executor.ts' | ||
| import { handleSecretMaintenanceRequest } from './maintenance-handler.ts' | ||
| import { getErrorMessage } from '#mcp/capabilities/error-message.ts' | ||
|
|
||
| const executeSmokeCode = 'async () => 42' | ||
| const executeSmokeProviders = [ | ||
| { | ||
| name: 'kody', | ||
| fns: {}, | ||
| kodyRemoteConnectors: [], | ||
| }, | ||
| ] as const | ||
|
Comment on lines
+6
to
+13
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
rg -n "ResolvedProvider" packages/worker/src/mcp/executor.ts -B2 -A8
rg -n "KodyRemoteConnectorMetadata" packages/worker/src/mcp/executor.ts -B2 -A8Repository: kentcdodds/kody Length of output: 4898 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== execute-maintenance.ts =="
wc -l packages/worker/src/execute-maintenance.ts
cat -n packages/worker/src/execute-maintenance.ts | sed -n '1,220p'
echo
echo "== kody-remote-types.ts =="
wc -l packages/worker/src/mcp/kody-remote-types.ts
cat -n packages/worker/src/mcp/kody-remote-types.ts | sed -n '1,220p'
echo
echo "== executor.ts relevant section =="
cat -n packages/worker/src/mcp/executor.ts | sed -n '390,460p'Repository: kentcdodds/kody Length of output: 6943 🏁 Script executed: #!/bin/bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
p = Path('packages/worker/src/execute-maintenance.ts')
text = p.read_text()
for i, line in enumerate(text.splitlines(), 1):
if 'executeSmoke' in line or 'getErrorMessage' in line or 'createKodyProviderProxySource' in line or 'providers:' in line or 'result.result' in line:
start = max(1, i-8)
end = min(len(text.splitlines()), i+16)
print(f"\n--- around line {i} ---")
for j in range(start, end+1):
print(f"{j}:{text.splitlines()[j-1]}")
PYRepository: kentcdodds/kody Length of output: 5995 🏁 Script executed: #!/bin/bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
for path in [
Path('packages/worker/src/execute-maintenance.ts'),
Path('packages/worker/src/mcp/kody-remote-types.ts'),
Path('packages/worker/src/mcp/executor.ts'),
]:
print(f"\n## {path} ##")
lines = path.read_text().splitlines()
for i, line in enumerate(lines, 1):
if 'executeSmoke' in line or 'getErrorMessage' in line or 'createKodyProviderProxySource' in line or 'KodyResolvedProvider' in line or 'kodyRemoteConnectors' in line or 'ResolvedProvider' in line:
start = max(1, i-6)
end = min(len(lines), i+14)
print(f"\n-- around line {i} --")
for j in range(start, end+1):
print(f"{j}:{lines[j-1]}")
PYRepository: kentcdodds/kody Length of output: 19496 🏁 Script executed: #!/bin/bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
files = [
'packages/worker/src/execute-maintenance.ts',
'packages/worker/src/mcp/kody-remote-types.ts',
'packages/worker/src/mcp/executor.ts',
]
for file in files:
print(f"\n## {file}")
lines = Path(file).read_text().splitlines()
for idx, line in enumerate(lines, 1):
if any(tok in line for tok in ['executeSmoke', 'getErrorMessage', 'createKodyProviderProxySource', 'KodyResolvedProvider', 'kodyRemoteConnectors', 'ResolvedProvider']):
start = max(1, idx - 6)
end = min(len(lines), idx + 14)
print(f"\n-- around line {idx} --")
for j in range(start, end + 1):
print(f"{j}:{lines[j-1]}")
PYRepository: kentcdodds/kody Length of output: 19487 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== execute-maintenance.ts =="
cat -n packages/worker/src/execute-maintenance.ts | sed -n '1,220p'Repository: kentcdodds/kody Length of output: 2229 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== getErrorMessage helper =="
fd -a "error-message.ts" packages/worker/src
if [ -f packages/worker/src/mcp/capabilities/error-message.ts ]; then
wc -l packages/worker/src/mcp/capabilities/error-message.ts
cat -n packages/worker/src/mcp/capabilities/error-message.ts | sed -n '1,200p'
fi
echo
echo "== TypeScript tool availability =="
command -v tsc || true
node -e "try { const ts = require('typescript'); console.log('typescript module:', ts.version); } catch (e) { console.log('typescript module unavailable') }"
echo
echo "== assignability probe =="
tmpdir="$(mktemp -d)"
cat > "$tmpdir/probe.ts" <<'TS'
type KodyRemoteConnectorMetadata = {
name: string
}
type ResolvedProvider = {
name: string
fns: Record<string, unknown>
kodyRemoteConnectors?: Array<KodyRemoteConnectorMetadata>
}
const providers = [
{
name: 'kody',
fns: {},
kodyRemoteConnectors: [],
},
] as const
const x: Array<ResolvedProvider> = [...providers]
TS
if command -v tsc >/dev/null 2>&1; then
tsc --noEmit --strict "$tmpdir/probe.ts" 2>&1 || true
else
echo "tsc not available"
fi
rm -rf "$tmpdir"Repository: kentcdodds/kody Length of output: 1732 🏁 Script executed: #!/bin/bash
set -euo pipefail
tmpdir="$(mktemp -d)"
cat > "$tmpdir/probe.ts" <<'TS'
type KodyRemoteConnectorMetadata = {
name: string
}
type ResolvedProvider = {
name: string
fns: Record<string, unknown>
kodyRemoteConnectors?: Array<KodyRemoteConnectorMetadata>
}
const executeSmokeProviders = [
{
name: 'kody',
fns: {},
kodyRemoteConnectors: [],
},
] as const
const providersA: Array<ResolvedProvider> = [...executeSmokeProviders]
const providersB: Array<ResolvedProvider> = executeSmokeProviders
declare const p: ResolvedProvider
const connectorsA: Array<KodyRemoteConnectorMetadata> = p.kodyRemoteConnectors!
const connectorsB: Array<KodyRemoteConnectorMetadata> = executeSmokeProviders[0].kodyRemoteConnectors
TS
tsc --noEmit --strict --ignoreConfig "$tmpdir/probe.ts" 2>&1 || true
rm -rf "$tmpdir"Repository: kentcdodds/kody Length of output: 951 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== assignability probe =="
tmpdir="$(mktemp -d)"
cat > "$tmpdir/probe.ts" <<'TS'
type KodyRemoteConnectorMetadata = {
name: string
}
type ResolvedProvider = {
name: string
fns: Record<string, unknown>
kodyRemoteConnectors?: Array<KodyRemoteConnectorMetadata>
}
const executeSmokeProviders = [
{
name: 'kody',
fns: {},
kodyRemoteConnectors: [],
},
] as const
const providersA: Array<ResolvedProvider> = [...executeSmokeProviders]
const providersB: Array<ResolvedProvider> = executeSmokeProviders
declare const p: ResolvedProvider
const connectorsA: Array<KodyRemoteConnectorMetadata> = p.kodyRemoteConnectors!
const connectorsB: Array<KodyRemoteConnectorMetadata> = executeSmokeProviders[0].kodyRemoteConnectors
TS
tsc --noEmit --strict --ignoreConfig "$tmpdir/probe.ts" 2>&1 || true
rm -rf "$tmpdir"
echo
echo "== ExecuteResult / getErrorMessage context =="
rg -n "type ExecuteResult|interface ExecuteResult|getErrorMessage\\(" packages/worker/src -B2 -A6Repository: kentcdodds/kody Length of output: 24337 Drop 🤖 Prompt for AI Agents |
||
|
|
||
| export async function runExecuteSmokeCheck(env: Env) { | ||
| if (!env.LOADER) { | ||
| throw new Error('Execute smoke check requires the LOADER binding.') | ||
| } | ||
| if (!workerExports?.KodyFetchGateway) { | ||
| throw new Error( | ||
| 'Execute smoke check requires the KodyFetchGateway worker export.', | ||
| ) | ||
| } | ||
|
|
||
| const origin = env.APP_BASE_URL?.trim() || 'https://heykody.dev' | ||
| const executor = createExecuteExecutor({ | ||
| env, | ||
| exports: workerExports, | ||
| gatewayProps: { | ||
| baseUrl: origin, | ||
| userId: null, | ||
| storageContext: null, | ||
| }, | ||
| timeoutMs: 10_000, | ||
| }) | ||
| const result = await executor.execute(executeSmokeCode, [ | ||
| ...executeSmokeProviders, | ||
| ]) | ||
| if (result.error) { | ||
| throw new Error(`Execute smoke check failed: ${result.error}`) | ||
| } | ||
| if (result.result !== 42) { | ||
| throw new Error( | ||
| `Execute smoke check returned unexpected result: ${getErrorMessage(result.result)}`, | ||
| ) | ||
| } | ||
| return { result: result.result } | ||
| } | ||
|
|
||
| export async function handleExecuteSmokeRequest( | ||
| request: Request, | ||
| env: Env, | ||
| ): Promise<Response> { | ||
| return handleSecretMaintenanceRequest({ | ||
| request, | ||
| secret: env.CAPABILITY_REINDEX_SECRET, | ||
| notConfiguredMessage: 'Execute smoke check is not configured', | ||
| run: async () => await runExecuteSmokeCheck(env), | ||
| }) | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,81 @@ | ||||||||||||||||||||||||||||||
| // Static factory source for dynamic execute modules. Do not serialize bundled | ||||||||||||||||||||||||||||||
| // functions with Function.prototype.toString() here — production wrangler/esbuild | ||||||||||||||||||||||||||||||
| // can rewrite named functions to use a bundle-scoped __name helper that does not | ||||||||||||||||||||||||||||||
| // exist inside dynamically loaded worker modules. | ||||||||||||||||||||||||||||||
| export const kodyRemoteProxyFactorySource = [ | ||||||||||||||||||||||||||||||
| 'function createKodyRemoteProxy(input) {', | ||||||||||||||||||||||||||||||
| ' const connectors = Object.fromEntries(', | ||||||||||||||||||||||||||||||
| ' input.remoteConnectors.map((connector) => [', | ||||||||||||||||||||||||||||||
| ' connector.name,', | ||||||||||||||||||||||||||||||
| ' {', | ||||||||||||||||||||||||||||||
| ' ...connector,', | ||||||||||||||||||||||||||||||
| ' capabilitiesByName: Object.fromEntries(', | ||||||||||||||||||||||||||||||
| ' connector.capabilities.map((capability) => [', | ||||||||||||||||||||||||||||||
| ' capability.name,', | ||||||||||||||||||||||||||||||
| ' capability,', | ||||||||||||||||||||||||||||||
| ' ]),', | ||||||||||||||||||||||||||||||
| ' ),', | ||||||||||||||||||||||||||||||
| ' },', | ||||||||||||||||||||||||||||||
| ' ]),', | ||||||||||||||||||||||||||||||
| ' );', | ||||||||||||||||||||||||||||||
| ' const formatNames = (names) =>', | ||||||||||||||||||||||||||||||
| ' names.length > 0', | ||||||||||||||||||||||||||||||
| ' ? names.map((name) => JSON.stringify(name)).join(", ")', | ||||||||||||||||||||||||||||||
| ' : "none";', | ||||||||||||||||||||||||||||||
| '', | ||||||||||||||||||||||||||||||
| ' return new Proxy(', | ||||||||||||||||||||||||||||||
| ' {},', | ||||||||||||||||||||||||||||||
| ' {', | ||||||||||||||||||||||||||||||
| ' get(_target, connectorName) {', | ||||||||||||||||||||||||||||||
| ' if (typeof connectorName === "symbol" || connectorName === "then") {', | ||||||||||||||||||||||||||||||
| ' return undefined;', | ||||||||||||||||||||||||||||||
| ' }', | ||||||||||||||||||||||||||||||
| ' const normalizedConnectorName = String(connectorName);', | ||||||||||||||||||||||||||||||
| ' const connector = connectors[normalizedConnectorName];', | ||||||||||||||||||||||||||||||
| ' if (!connector) {', | ||||||||||||||||||||||||||||||
| ' throw new Error(', | ||||||||||||||||||||||||||||||
| ' "Unknown remote connector \\"" + normalizedConnectorName + "\\". Available remote connectors: " + formatNames(Object.keys(connectors)) + ".",', | ||||||||||||||||||||||||||||||
| ' );', | ||||||||||||||||||||||||||||||
| ' }', | ||||||||||||||||||||||||||||||
| ' return new Proxy(', | ||||||||||||||||||||||||||||||
| ' {},', | ||||||||||||||||||||||||||||||
| ' {', | ||||||||||||||||||||||||||||||
| ' get(_connectorTarget, capabilityName) {', | ||||||||||||||||||||||||||||||
| ' if (', | ||||||||||||||||||||||||||||||
| ' typeof capabilityName === "symbol" ||', | ||||||||||||||||||||||||||||||
| ' capabilityName === "then"', | ||||||||||||||||||||||||||||||
| ' ) {', | ||||||||||||||||||||||||||||||
| ' return undefined;', | ||||||||||||||||||||||||||||||
| ' }', | ||||||||||||||||||||||||||||||
| ' const normalizedCapabilityName = String(capabilityName);', | ||||||||||||||||||||||||||||||
| ' if (', | ||||||||||||||||||||||||||||||
| ' !connector.status.connected ||', | ||||||||||||||||||||||||||||||
| ' connector.status.toolCount === 0', | ||||||||||||||||||||||||||||||
| ' ) {', | ||||||||||||||||||||||||||||||
| ' throw new Error(connector.status.unavailableMessage);', | ||||||||||||||||||||||||||||||
| ' }', | ||||||||||||||||||||||||||||||
| ' const capability =', | ||||||||||||||||||||||||||||||
| ' connector.capabilitiesByName[normalizedCapabilityName];', | ||||||||||||||||||||||||||||||
| ' if (!capability) {', | ||||||||||||||||||||||||||||||
| ' throw new Error(', | ||||||||||||||||||||||||||||||
| ' "Unknown remote capability \\"" + normalizedCapabilityName + "\\" for connector \\"" + normalizedConnectorName + "\\". Available capabilities: " + formatNames(connector.capabilities.map((entry) => entry.name)) + ".",', | ||||||||||||||||||||||||||||||
| ' );', | ||||||||||||||||||||||||||||||
| ' }', | ||||||||||||||||||||||||||||||
| ' return async (args) =>', | ||||||||||||||||||||||||||||||
| ' await input.callTool(capability.dispatchName, args);', | ||||||||||||||||||||||||||||||
| ' },', | ||||||||||||||||||||||||||||||
| ' },', | ||||||||||||||||||||||||||||||
| ' );', | ||||||||||||||||||||||||||||||
| ' },', | ||||||||||||||||||||||||||||||
| ' },', | ||||||||||||||||||||||||||||||
| ' );', | ||||||||||||||||||||||||||||||
| '}', | ||||||||||||||||||||||||||||||
| ].join('\n') | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| export function assertGeneratedExecutorSourceIsBundleSafe(source: string) { | ||||||||||||||||||||||||||||||
| if (/\b__name\b/u.test(source)) { | ||||||||||||||||||||||||||||||
| throw new Error( | ||||||||||||||||||||||||||||||
| 'Generated executor source must not reference bundle-scoped __name helpers.', | ||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
Comment on lines
+75
to
+81
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Regex can false-positive on interpolated data, not just bundler output. This helper is invoked against fully-assembled source that includes JSON-serialized connector/capability metadata ( Real esbuild/wrangler output always uses 🐛 Proposed fix export function assertGeneratedExecutorSourceIsBundleSafe(source: string) {
- if (/\b__name\b/u.test(source)) {
+ if (/\b__name\s*\(/u.test(source)) {
throw new Error(
'Generated executor source must not reference bundle-scoped __name helpers.',
)
}
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: kentcdodds/kody
Length of output: 3921
🏁 Script executed:
Repository: kentcdodds/kody
Length of output: 193
Retry loop can exit on the first curl failure.
status="$(curl ...)"runs underset -euo pipefail, so a transient connection/timeout error aborts the step before any retry happens. Wrap it so curl failures become a retryable status instead of ending the step immediately.🤖 Prompt for AI Agents