Skip to content

Commit 7c114ae

Browse files
committed
fix(functions-runner): force re-evaluation of imported input files
1 parent e5dc106 commit 7c114ae

2 files changed

Lines changed: 162 additions & 1 deletion

File tree

scripts/functions-runner.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ type Manifest = {
8383
};
8484

8585
type EvalRegistry = NonNullable<typeof globalThis._evals>;
86+
let moduleImportNonce = 0;
8687

8788
function freshRegistry(): EvalRegistry {
8889
return {
@@ -115,6 +116,15 @@ function currentRegistry(fallback: EvalRegistry): EvalRegistry {
115116
};
116117
}
117118

119+
function buildIsolatedImportUrl(absolutePath: string): string {
120+
const moduleUrl = pathToFileURL(absolutePath);
121+
// Force top-level evaluation for each input file, even if imported earlier
122+
// as a dependency while processing a previous input file.
123+
moduleUrl.searchParams.set("bt_runner_input_nonce", `${moduleImportNonce}`);
124+
moduleImportNonce += 1;
125+
return moduleUrl.href;
126+
}
127+
118128
async function collectFunctionEvents(
119129
items: EventRegistryItem[],
120130
includeLegacyPrompts: boolean,
@@ -316,7 +326,7 @@ async function processFile(filePath: string): Promise<ManifestFile> {
316326
globalThis._evals = fallbackRegistry;
317327
globalThis._lazy_load = true;
318328

319-
await import(pathToFileURL(absolutePath).href);
329+
await import(buildIsolatedImportUrl(absolutePath));
320330
const registry = currentRegistry(fallbackRegistry);
321331

322332
const entries: Array<CodeEntry | FunctionEventEntry> = [

tests/functions.rs

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -902,6 +902,157 @@ globalThis._evals.functions.push({
902902
);
903903
}
904904

905+
#[test]
906+
fn functions_js_runner_reexecutes_imported_input_files() {
907+
if !command_exists("node") {
908+
eprintln!(
909+
"Skipping functions_js_runner_reexecutes_imported_input_files (node not installed)."
910+
);
911+
return;
912+
}
913+
let Some(tsc) = find_tsc() else {
914+
eprintln!(
915+
"Skipping functions_js_runner_reexecutes_imported_input_files (tsc not installed)."
916+
);
917+
return;
918+
};
919+
920+
let root = repo_root();
921+
let tmp = tempdir().expect("tempdir");
922+
let sample_b_path = tmp.path().join("sample-b.mjs");
923+
std::fs::write(
924+
&sample_b_path,
925+
r#"globalThis._evals ??= { functions: [], prompts: [], parameters: [], evaluators: {}, reporters: {} };
926+
globalThis._evals.functions.push({
927+
name: "js-tool-b",
928+
slug: "js-tool-b",
929+
type: "tool",
930+
preview: "export function b() { return 2; }"
931+
});
932+
export const b = 2;
933+
"#,
934+
)
935+
.expect("write sample-b.mjs");
936+
937+
let sample_a_path = tmp.path().join("sample-a.mjs");
938+
std::fs::write(
939+
&sample_a_path,
940+
r#"import "./sample-b.mjs";
941+
globalThis._evals ??= { functions: [], prompts: [], parameters: [], evaluators: {}, reporters: {} };
942+
globalThis._evals.functions.push({
943+
name: "js-tool-a",
944+
slug: "js-tool-a",
945+
type: "tool",
946+
preview: "export function a() { return 1; }"
947+
});
948+
"#,
949+
)
950+
.expect("write sample-a.mjs");
951+
952+
let runner_dir = tmp.path().join("runner");
953+
let compile_output = Command::new(&tsc)
954+
.current_dir(&root)
955+
.args([
956+
"scripts/functions-runner.ts",
957+
"scripts/runner-common.ts",
958+
"--module",
959+
"esnext",
960+
"--target",
961+
"es2020",
962+
"--moduleResolution",
963+
"bundler",
964+
"--outDir",
965+
])
966+
.arg(&runner_dir)
967+
.output()
968+
.expect("compile functions runner");
969+
if !compile_output.status.success() {
970+
let stderr = String::from_utf8_lossy(&compile_output.stderr);
971+
panic!("tsc failed for functions runner:\n{stderr}");
972+
}
973+
974+
let runner_js = runner_dir.join("functions-runner.js");
975+
let runner_common_js = runner_dir.join("runner-common.js");
976+
assert!(runner_js.is_file(), "compiled functions-runner.js missing");
977+
assert!(
978+
runner_common_js.is_file(),
979+
"compiled runner-common.js missing"
980+
);
981+
982+
let runner_code = std::fs::read_to_string(&runner_js).expect("read compiled runner");
983+
let patched_runner_code = runner_code
984+
.replace("\"./runner-common\"", "\"./runner-common.js\"")
985+
.replace("'./runner-common'", "'./runner-common.js'");
986+
assert_ne!(
987+
runner_code, patched_runner_code,
988+
"compiled runner import path did not contain ./runner-common"
989+
);
990+
std::fs::write(&runner_js, patched_runner_code).expect("write patched compiled runner");
991+
std::fs::write(runner_dir.join("package.json"), r#"{ "type": "module" }"#)
992+
.expect("write runner package.json");
993+
994+
let output = Command::new("node")
995+
.arg(&runner_js)
996+
.arg(&sample_a_path)
997+
.arg(&sample_b_path)
998+
.output()
999+
.expect("run compiled functions runner");
1000+
if !output.status.success() {
1001+
let stderr = String::from_utf8_lossy(&output.stderr);
1002+
panic!("compiled functions runner failed:\n{stderr}");
1003+
}
1004+
1005+
let manifest: Value = serde_json::from_slice(&output.stdout).expect("parse manifest JSON");
1006+
let files = manifest["files"].as_array().expect("files array");
1007+
assert_eq!(files.len(), 2, "expected two manifest files");
1008+
1009+
let sample_a_canonical = sample_a_path
1010+
.canonicalize()
1011+
.expect("canonicalize sample-a.mjs");
1012+
let sample_b_canonical = sample_b_path
1013+
.canonicalize()
1014+
.expect("canonicalize sample-b.mjs");
1015+
let mut files_by_source = BTreeMap::new();
1016+
for file in files {
1017+
let source_file = file
1018+
.get("source_file")
1019+
.and_then(Value::as_str)
1020+
.expect("source_file");
1021+
let canonical_source = PathBuf::from(source_file)
1022+
.canonicalize()
1023+
.expect("canonicalize manifest source_file");
1024+
files_by_source.insert(canonical_source, file);
1025+
}
1026+
1027+
let file_a = files_by_source
1028+
.get(&sample_a_canonical)
1029+
.expect("manifest file for sample-a.mjs");
1030+
let entries_a = file_a
1031+
.get("entries")
1032+
.and_then(Value::as_array)
1033+
.expect("sample-a entries");
1034+
assert!(
1035+
entries_a
1036+
.iter()
1037+
.any(|entry| { entry.get("slug").and_then(Value::as_str) == Some("js-tool-a") }),
1038+
"expected sample-a.mjs entries to include js-tool-a"
1039+
);
1040+
1041+
let file_b = files_by_source
1042+
.get(&sample_b_canonical)
1043+
.expect("manifest file for sample-b.mjs");
1044+
let entries_b = file_b
1045+
.get("entries")
1046+
.and_then(Value::as_array)
1047+
.expect("sample-b entries");
1048+
assert!(
1049+
entries_b
1050+
.iter()
1051+
.any(|entry| { entry.get("slug").and_then(Value::as_str) == Some("js-tool-b") }),
1052+
"expected sample-b.mjs entries to include js-tool-b"
1053+
);
1054+
}
1055+
9051056
#[test]
9061057
fn functions_python_runner_emits_valid_manifest_with_bundle() {
9071058
let Some(python) = find_python() else {

0 commit comments

Comments
 (0)