Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion docs/internal/session-state.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,22 @@
# Session State

Last updated: 2026-01-03
Last updated: 2026-10-30

## Native CI qualification fixes (2026-10-30)

Local native CI exposed three defects: `--no-prompt` omitted declared defaults,
unknown templates still entered interactive selection, and Windows backslashes
escaped Velocity path variables. Defaults now apply with validated explicit
overrides; unknown templates return NOT_FOUND without reading stdin; path
separators are normalized before interpolation. Interactive fallback remains
covered separately.

Four non-interactive regressions and a Windows-path regression failed before the
fixes. On macOS27.2 ARM64/GraalVM21.0.12, full Maven verification, native packaging
and verification with the built binary passed235 tests across26 reports with
zero failures/errors/skips. Native tests must run after building the binary;
the earlier build-before-test arrangement silently skipped10 native E2E cases.
Exact-commit Gitea macOS/Windows verification and normal PR review remain pending.

## Current Status

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,82 @@ void testInit_skipHooks_noHooksExecuted()



/***************************************************************************
* Non-interactive generation applies defaults without consuming stdin.
*
* @throws IOException if the generated file cannot be read
* @since 0.2.1
***************************************************************************/
@Test
void testInit_noPrompt_usesDefaults() throws IOException
{
Path outputDir = tempDir.resolve("default-values");
harness.execute("qqq", "init", "test-with-prompts", "--no-prompt",
"-o", outputDir.toString());

assertThat(harness.getExitCode()).as(harness.getStderr()).isEqualTo(ExitCodes.SUCCESS);
assertThat(Files.readString(outputDir.resolve("README.md")))
.contains("my-project", "com.example");
}



/***************************************************************************
* Explicit variables override defaults while omitted variables retain them.
*
* @throws IOException if the generated file cannot be read
* @since 0.2.1
***************************************************************************/
@Test
void testInit_noPrompt_overridesOneDefault() throws IOException
{
Path outputDir = tempDir.resolve("override-default");
harness.execute("qqq", "init", "test-with-prompts", "--no-prompt",
"--var", "projectName=custom-project", "-o", outputDir.toString());

assertThat(harness.getExitCode()).as(harness.getStderr()).isEqualTo(ExitCodes.SUCCESS);
assertThat(Files.readString(outputDir.resolve("README.md")))
.contains("custom-project", "com.example");
}



/***************************************************************************
* An unknown template must not fall back to an interactive selection.
*
* @since 0.2.1
***************************************************************************/
@Test
void testInit_noPrompt_unknownTemplateReturnsNotFound()
{
Path outputDir = tempDir.resolve("unknown-template");
harness.execute("qqq", "init", "missing-template", "--no-prompt",
"-o", outputDir.toString());

assertThat(harness.getExitCode()).isEqualTo(ExitCodes.NOT_FOUND);
assertThat(outputDir).doesNotExist();
}



/***************************************************************************
* Invalid required overrides must fail instead of prompting or using defaults.
*
* @since 0.2.1
***************************************************************************/
@Test
void testInit_noPrompt_rejectsEmptyRequiredOverride()
{
Path outputDir = tempDir.resolve("invalid-override");
harness.execute("qqq", "init", "test-with-prompts", "--no-prompt",
"--var", "projectName=", "-o", outputDir.toString());

assertThat(harness.getExitCode()).isEqualTo(ExitCodes.VALIDATION);
assertThat(outputDir).doesNotExist();
}



/***************************************************************************
* Test combined flags work together.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -613,20 +613,19 @@ void testInit_generatedPom_isValidXml() throws Exception
/***************************************************************************
* Test behavior when requesting a template that doesn't exist.
*
* With TestTemplateRegistry, non-existent templates cause the system
* to fall back to the first available template in --no-prompt mode.
* Interactive selection can accept the first available fallback template.
*
* @since 0.2.0
***************************************************************************/
@Test
void testInit_unknownTemplate_fallsBackToFirst() throws IOException
void testInit_unknownTemplate_interactiveFallbackToFirst() throws IOException
{
Path outputDir = tempDir.resolve("unknown-template-test");

//////////////////////////////////////////////////////////////////////////
// With --no-prompt, requesting unknown template uses first available //
// The empty input accepts the first interactive selection. //
//////////////////////////////////////////////////////////////////////////
harness.execute("qqq", "init", "non-existent-template", "--no-prompt",
harness.execute("qqq", "init", "non-existent-template",
"-o", outputDir.toString());

//////////////////////////////////////////////////////////////////////////
Expand Down
22 changes: 22 additions & 0 deletions qctl-qqq/src/main/java/io/qrun/qctl/qqq/InitCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
import io.qrun.qctl.qqq.template.MergeOptions;
import io.qrun.qctl.qqq.template.PostGenHookRunner;
import io.qrun.qctl.qqq.template.PromptRunner;
import io.qrun.qctl.qqq.template.PromptValidator;
import io.qrun.qctl.qqq.template.TemplateEngine;
import io.qrun.qctl.qqq.template.TemplateManifest;
import io.qrun.qctl.qqq.template.TemplateRenderException;
Expand Down Expand Up @@ -172,6 +173,21 @@ public Integer call()

// Step 4: Collect variables (prompts + CLI overrides)
Map<String, String> allVars = new LinkedHashMap<>();
if(noPrompt && manifest.prompts() != null)
{
PromptValidator validator = new PromptValidator();
for(TemplateManifest.Prompt prompt : manifest.prompts())
{
String value = variables.getOrDefault(prompt.name(), prompt.defaultValue());
PromptValidator.ValidationResult result = validator.validate(prompt, value);
if(!result.valid())
{
ui.error(result.errorMessage());
return ExitCodes.VALIDATION;
}
allVars.put(prompt.name(), value);
}
}
if(!noPrompt && manifest.prompts() != null && !manifest.prompts().isEmpty())
{
PromptRunner promptRunner = new PromptRunner();
Expand Down Expand Up @@ -318,6 +334,12 @@ private TemplateInfo selectTemplate(TemplateRegistry registry, ConsoleUI ui) thr
List<String> templateIds = templates.stream().map(TemplateInfo::id).toList();
SuggestionEngine suggestionEngine = new SuggestionEngine();
List<String> suggestions = suggestionEngine.findSimilar(templateName, templateIds);
if(noPrompt)
{
throw new TemplateError(TemplateError.ErrorCategory.TEMPLATE_NOT_FOUND,
"Template '" + templateName + "' not found.", suggestions,
"Use 'qctl qqq list' to list available templates");
}

ui.error("Template '" + templateName + "' not found.");
if(!suggestions.isEmpty())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -409,7 +409,9 @@ public String renderPath(String path, Map<String, String> variables)
// This allows templates to use __VARNAME__ in paths, which is safer //
// for shells and build tools that might interpret $ as a variable. //
/////////////////////////////////////////////////////////////////////////
String normalizedPath = path.replaceAll("__([a-zA-Z][a-zA-Z0-9]*)__", "\\$$1");
// Windows separators otherwise escape the following Velocity variable.
String normalizedPath = path.replace('\\', '/')
.replaceAll("__([a-zA-Z][a-zA-Z0-9]*)__", "\\$$1");
return renderContent(normalizedPath, variables);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;


Expand Down Expand Up @@ -44,11 +45,19 @@ public class TemplateResolver
public Path resolve(String source, String version) throws IOException
{
// Local path
Path localPath = Path.of(source);
if(Files.isDirectory(localPath))
try
{
Path localPath = Path.of(source);
if(Files.isDirectory(localPath))
{
System.out.println("Using local template: " + localPath.toAbsolutePath());
return localPath.toAbsolutePath();
}
}
catch(InvalidPathException e)
{
System.out.println("Using local template: " + localPath.toAbsolutePath());
return localPath.toAbsolutePath();
// Remote URLs are not valid filesystem paths on Windows.
// Validate them below using the supported Git URL formats.
}

// Git URL or GitHub shorthand
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,24 @@ void testRenderDirectoryWithPathVariables() throws IOException, TemplateRenderEx



/***************************************************************************
* Windows separators must not escape path placeholders in Velocity.
*
* @throws TemplateRenderException if rendering fails
* @since 0.2.1
***************************************************************************/
@Test
void testRenderWindowsPathVariables() throws TemplateRenderException
{
Map<String, String> variables = Map.of("packagePath", "com/acme/orders");
assertThat(engine.renderPath("src\\main\\java\\$packagePath\\App.java", variables))
.isEqualTo("src/main/java/com/acme/orders/App.java");
assertThat(engine.renderPath("src\\main\\java\\__packagePath__\\App.java", variables))
.isEqualTo("src/main/java/com/acme/orders/App.java");
}



/***************************************************************************
* Test binary files are copied without modification.
***************************************************************************/
Expand Down
Loading