mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-09 03:20:56 +00:00
Anchor run-goal template validation at the version entrypoint
Create-time validation of workflow.toml run goals anchored includes at workflow.toml for inline goals and at the goal file's directory for file goals, while the run engine inlines the effective goal into the entrypoint graph and renders it under the entrypoint's template source. That divergence rejected layouts `fabro run` executes fine and accepted layouts that fail at render time. Anchor both goal forms at the entrypoint so validation matches the runtime, and pin the anchor with a nested-entrypoint test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
5a5cfbdaa0
commit
2f2097be54
1 changed files with 51 additions and 9 deletions
|
|
@ -167,18 +167,21 @@ fn validate_config(
|
|||
validate_dockerfile(version, &config_path, image)?;
|
||||
}
|
||||
|
||||
// The run engine inlines the effective goal (file contents included) into
|
||||
// the entrypoint graph and renders it under the entrypoint's template
|
||||
// source, so goal includes anchor at the entrypoint for both goal forms.
|
||||
match layer.run.as_ref().and_then(|run| run.goal.as_ref()) {
|
||||
Some(RunGoalLayer::Inline(goal)) => {
|
||||
template_roots.push(&config_path, unresolved_source(goal));
|
||||
template_roots.push(version.entrypoint(), unresolved_source(goal));
|
||||
}
|
||||
Some(RunGoalLayer::File { file }) => {
|
||||
let (target, content) = validate_config_file_reference(
|
||||
let (_, content) = validate_config_file_reference(
|
||||
version,
|
||||
&config_path,
|
||||
ReferenceKind::RunGoalFile,
|
||||
&unresolved_source(file),
|
||||
)?;
|
||||
template_roots.push(&target, content);
|
||||
template_roots.push(version.entrypoint(), content);
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
|
|
@ -568,9 +571,12 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn accepts_file_workflow_goal_with_transitive_template_closure() {
|
||||
// The goal file's own includes anchor at the entrypoint's directory
|
||||
// (the package root here), not at the goal file's directory; loaded
|
||||
// dependencies then anchor at their own directories as usual.
|
||||
let version =
|
||||
version_with_config("_version = 1\n[run.goal]\nfile = \"prompts/goal.md\"\n", [
|
||||
("prompts/goal.md", r#"{% include "partial.md" %}"#),
|
||||
("prompts/goal.md", r#"{% include "prompts/partial.md" %}"#),
|
||||
("prompts/partial.md", r#"{% include "nested/detail.md" %}"#),
|
||||
("prompts/nested/detail.md", "Use {{ vars.detail }}"),
|
||||
])
|
||||
|
|
@ -579,6 +585,43 @@ mod tests {
|
|||
assert_eq!(version.version().files().len(), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn anchors_workflow_goal_includes_at_the_entrypoint() {
|
||||
let version_with_entrypoint = |goal_include_target: &'static str| {
|
||||
ValidatedWorkflowVersion::new(
|
||||
WorkflowVersion::new(
|
||||
path("graphs/main.fabro"),
|
||||
BTreeMap::from([
|
||||
(path("graphs/main.fabro"), "digraph W {}".to_owned()),
|
||||
(
|
||||
path("workflow.toml"),
|
||||
"_version = 1\n[run]\ngoal = \"{% include \\\"shared.md\\\" %}\"\n"
|
||||
.to_owned(),
|
||||
),
|
||||
(path(goal_include_target), "shared".to_owned()),
|
||||
]),
|
||||
BTreeMap::default(),
|
||||
)
|
||||
.expect("test fixtures must be structurally valid"),
|
||||
)
|
||||
};
|
||||
|
||||
// The include resolves beside the entrypoint graph, matching where
|
||||
// the run engine renders the inlined goal.
|
||||
version_with_entrypoint("graphs/shared.md").unwrap();
|
||||
|
||||
let error = version_with_entrypoint("shared.md").unwrap_err();
|
||||
assert!(matches!(
|
||||
error,
|
||||
WorkflowVersionError::Template { path: source_path, source }
|
||||
if source_path == path("graphs/main.fabro")
|
||||
&& matches!(
|
||||
source.as_ref(),
|
||||
TemplateDiscoveryError::Missing { reference, .. } if reference == "shared.md"
|
||||
)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_non_static_or_nonportable_workflow_goal_file_references() {
|
||||
for reference in ["{{ vars.NAME }}", "{% include \"goal.md\" %}"] {
|
||||
|
|
@ -630,11 +673,11 @@ mod tests {
|
|||
else {
|
||||
panic!("expected missing template dependency");
|
||||
};
|
||||
assert_eq!(source_path, path("workflow.toml"));
|
||||
assert_eq!(source_path, path("workflow.fabro"));
|
||||
assert!(matches!(
|
||||
source.as_ref(),
|
||||
TemplateDiscoveryError::Missing { parent, reference }
|
||||
if parent.to_string() == "workflow.toml" && reference == "missing.md"
|
||||
if parent.to_string() == "workflow.fabro" && reference == "missing.md"
|
||||
));
|
||||
|
||||
let dynamic = version_with_inline_goal(r"{% include inputs.partial %}", []).unwrap_err();
|
||||
|
|
@ -644,7 +687,7 @@ mod tests {
|
|||
assert!(matches!(
|
||||
source.as_ref(),
|
||||
TemplateDiscoveryError::Dynamic { parent }
|
||||
if parent.to_string() == "workflow.toml"
|
||||
if parent.to_string() == "workflow.fabro"
|
||||
));
|
||||
|
||||
let escaping =
|
||||
|
|
@ -657,8 +700,7 @@ mod tests {
|
|||
TemplateDiscoveryError::Load {
|
||||
source: TemplateLoadError::EscapesRoot { parent, .. },
|
||||
..
|
||||
}
|
||||
if parent.to_string() == "workflow.toml"
|
||||
} if parent.to_string() == "workflow.fabro"
|
||||
));
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue