diff --git a/labs/22-yield/yield/cmd/yskill/main_test.go b/labs/22-yield/yield/cmd/yskill/main_test.go index 4bbf1be8..26042f22 100644 --- a/labs/22-yield/yield/cmd/yskill/main_test.go +++ b/labs/22-yield/yield/cmd/yskill/main_test.go @@ -5,6 +5,7 @@ import ( "flag" "io" "os" + "os/exec" "path/filepath" "runtime" "runtime/debug" @@ -210,8 +211,10 @@ func TestScaffoldSkillWritesLanguageSpecificEntrypoints(t *testing.T) { t.Fatalf("skill.json = version %d language %q", generated.Version, generated.Language) } skill := readTestFile(t, filepath.Join(dir, "SKILL.md")) - if !strings.Contains(skill, tt.command) { - t.Fatalf("SKILL.md does not contain %q:\n%s", tt.command, skill) + workflow := shellQuoteForPlatform(dir, runtime.GOOS) + command := strings.Replace(tt.command, " .", " "+workflow, 1) + if !strings.Contains(skill, command) { + t.Fatalf("SKILL.md does not contain %q:\n%s", command, skill) } entrypoint := map[string]string{ "typescript": "main.ts", @@ -244,6 +247,97 @@ func TestScaffoldSkillWritesLanguageSpecificEntrypoints(t *testing.T) { } } +func TestPackageScaffoldsPrintCreatedWorkflowInNextCommands(t *testing.T) { + previousVersion := version + version = "0.1.28" + t.Cleanup(func() { version = previousVersion }) + + for _, tt := range []struct { + language string + launcher string + }{ + {language: "typescript", launcher: "npm exec -- yskill"}, + {language: "python", launcher: "python -m yieldskill"}, + } { + t.Run(tt.language, func(t *testing.T) { + root := filepath.Join(t.TempDir(), "project with space") + dir := filepath.Join(root, "skills", "safe-change") + output := captureStdout(t, func() { + if err := scaffoldSkill(dir, tt.language, "", "Check a safe change before applying it."); err != nil { + t.Fatal(err) + } + }) + workflow := shellQuoteForPlatform(dir, runtime.GOOS) + for _, line := range []string{ + "test: " + tt.launcher + " doctor " + workflow + " --test", + "then: " + tt.launcher + " register " + workflow, + } { + if !strings.Contains(output, line) { + t.Fatalf("init output does not contain %q:\n%s", line, output) + } + } + }) + } +} + +func TestShellQuoteForPlatform(t *testing.T) { + for _, tt := range []struct { + name, value, goos, want string + }{ + {name: "relative", value: "skills/review", goos: "linux", want: "'skills/review'"}, + {name: "explicit relative", value: "./skills/review", goos: "linux", want: "'./skills/review'"}, + {name: "absolute", value: "/tmp/project/skills/review", goos: "linux", want: "'/tmp/project/skills/review'"}, + {name: "nested with space", value: "workflows/team one/safe change's", goos: "linux", want: `'workflows/team one/safe change'"'"'s'`}, + {name: "windows", value: `skills\safe change's`, goos: "windows", want: `'skills\safe change''s'`}, + } { + t.Run(tt.name, func(t *testing.T) { + if got := shellQuoteForPlatform(tt.value, tt.goos); got != tt.want { + t.Fatalf("quote = %q, want %q", got, tt.want) + } + }) + } +} + +func TestRustScaffoldNamesPrimaryBinary(t *testing.T) { + previousVersion := version + version = "0.1.28" + t.Cleanup(func() { version = previousVersion }) + dir := filepath.Join(t.TempDir(), "safe-change") + if err := scaffoldSkill(dir, "rust", "", "Check a safe change before applying it."); err != nil { + t.Fatal(err) + } + manifest := readTestFile(t, filepath.Join(dir, "skill.json")) + if manifest != "{\"version\":1,\"language\":\"rust\",\"run\":[\"cargo\",\"run\",\"--quiet\",\"--bin\",\"safe-change\"]}\n" { + t.Fatalf("skill.json = %q", manifest) + } +} + +func TestRustScaffoldRunsPrimaryBinaryWhenFixtureAddsAnotherBinary(t *testing.T) { + if _, err := exec.LookPath("cargo"); err != nil { + t.Skip("cargo is not installed") + } + dir := filepath.Join(t.TempDir(), "safe-change") + if err := scaffoldSkill(dir, "rust", "", "Check a safe change before applying it."); err != nil { + t.Fatal(err) + } + writeTestFile(t, filepath.Join(dir, "Cargo.toml"), "[package]\nname = \"safe-change\"\nversion = \"0.1.0\"\nedition = \"2021\"\n") + writeTestFile(t, filepath.Join(dir, "src", "main.rs"), "fn main() { println!(\"workflow\"); }\n") + writeTestFile(t, filepath.Join(dir, "src", "bin", "fixture-helper.rs"), "fn main() { println!(\"fixture\"); }\n") + manifest, err := readSkillManifest(dir) + if err != nil { + t.Fatal(err) + } + cmd := exec.Command(manifest.Run[0], manifest.Run[1:]...) + cmd.Dir = dir + out, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("generated run command failed: %v\n%s", err, out) + } + if strings.TrimSpace(string(out)) != "workflow" { + t.Fatalf("generated run command selected %q", out) + } +} + func TestGoScaffoldCanResolveItsPinnedModuleOnFirstRun(t *testing.T) { previousVersion := version previousTidyGoModule := tidyGoModule @@ -285,7 +379,7 @@ func TestLocalGoAndRustScaffoldsKeepTheInvokedRuntime(t *testing.T) { } skill := readTestFile(t, filepath.Join(dir, "SKILL.md")) launcher := repositoryRuntimeLauncher(filepath.Join(".yield", "bin", filepath.Base(localRuntimePath(repo))), runtime.GOOS) - workflow := shellQuote("skills/safe-change") + workflow := shellQuote(filepath.Join("skills", "safe-change")) for _, command := range []string{ launcher + " run " + workflow, launcher + " respond --value --skill " + workflow, diff --git a/labs/22-yield/yield/cmd/yskill/scaffold.go b/labs/22-yield/yield/cmd/yskill/scaffold.go index fdbde069..10cb362c 100644 --- a/labs/22-yield/yield/cmd/yskill/scaffold.go +++ b/labs/22-yield/yield/cmd/yskill/scaffold.go @@ -45,7 +45,7 @@ func scaffoldCommand(language, dir string) (launcher, workflow string) { "go": "yskill", "rust": "yskill", }[language] - workflow = "." + workflow = shellQuoteForPlatform(dir, runtime.GOOS) if language != "go" && language != "rust" { return launcher, workflow } @@ -65,7 +65,14 @@ func scaffoldCommand(language, dir string) (launcher, workflow string) { if err != nil { return launcher, workflow } - return repositoryRuntimeLauncher(runtimeRel, runtime.GOOS), shellQuote(filepath.ToSlash(skillRel)) + return repositoryRuntimeLauncher(runtimeRel, runtime.GOOS), shellQuoteForPlatform(skillRel, runtime.GOOS) +} + +func shellQuoteForPlatform(value, goos string) string { + if goos == "windows" { + return "'" + strings.ReplaceAll(value, "'", "''") + "'" + } + return shellQuote(filepath.ToSlash(value)) } func scaffoldSkill(dir, language, sdkPath, description string) error { @@ -155,7 +162,7 @@ func scaffoldFiles(name, language, sdkPath string) map[string]string { ".cargo/config.toml": "[registries.operatorstack]\nindex = \"sparse+https://get.operatorstack.systems/cargo/index/\"\n", "Cargo.toml": fmt.Sprintf("[package]\nname = %q\nversion = \"0.1.0\"\nedition = \"2021\"\n\n[dependencies]\nyieldskill = { version = \"=%s\", registry = \"operatorstack\" }\nserde_json = \"1\"\n", name, v), "src/main.rs": mainRust, - "skill.json": "{\"version\":1,\"language\":\"rust\",\"run\":[\"cargo\",\"run\",\"--quiet\"]}\n", + "skill.json": fmt.Sprintf("{\"version\":1,\"language\":\"rust\",\"run\":[\"cargo\",\"run\",\"--quiet\",\"--bin\",%q]}\n", name), } default: gomod := fmt.Sprintf("module %s\n\ngo 1.26.5\n\nrequire github.com/operatorstack/yield v%s\n", name, v) diff --git a/labs/22-yield/yield/docs/testing-fixtures.md b/labs/22-yield/yield/docs/testing-fixtures.md index f8f4c170..c6b9e645 100644 --- a/labs/22-yield/yield/docs/testing-fixtures.md +++ b/labs/22-yield/yield/docs/testing-fixtures.md @@ -24,3 +24,7 @@ first. `teardown` runs after success or failure. Every hook receives Keep hooks small and repeatable. They should prepare or clean fixture state, not replace the workflow behavior being tested. + +For Rust, a fixture helper may add another binary under `src/bin/`. New +workflows name the primary workflow binary in `skill.json`, so Cargo still +runs the workflow without asking you to choose a binary. diff --git a/labs/22-yield/yield/evals/results/latest.json b/labs/22-yield/yield/evals/results/latest.json index df3310f4..45b828e6 100644 --- a/labs/22-yield/yield/evals/results/latest.json +++ b/labs/22-yield/yield/evals/results/latest.json @@ -1,8 +1,8 @@ { "schema_version": 2, "methodology_version": "1.1", - "generated_at": "2026-08-02T16:31:06.974Z", - "source_digest": "7e61b35562440679e70f58f61a73d06e13d6b823b72b36498a48a9156775dc20", + "generated_at": "2026-08-02T19:51:34.931Z", + "source_digest": "e5b21f13b239b762064fa3a4bd3d2354fba3dc46feb324f9bddbccad72827c24", "status": "passed", "workflow_conformance": { "passed": 40, diff --git a/labs/22-yield/yield/release-notes/2026-08-02-final-dx-pass.md b/labs/22-yield/yield/release-notes/2026-08-02-final-dx-pass.md new file mode 100644 index 00000000..2b77b246 --- /dev/null +++ b/labs/22-yield/yield/release-notes/2026-08-02-final-dx-pass.md @@ -0,0 +1,6 @@ +# Yield 0.1.28 final DX pass + +- Print the created workflow path in TypeScript and Python `init` follow-up commands. +- Quote generated workflow paths for the active shell. +- Name the primary Rust binary in new workflow manifests so fixture helpers do not make `cargo run` ambiguous. +- Keep runtime and SDK behavior unchanged.