story-kit: start 65_story_standardised_script_test_entry_point_for_all_projects
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: Standardised script/test Entry Point for All Projects
|
name: Standardised script/test Entry Point for All Projects
|
||||||
test_plan: pending
|
test_plan: approved
|
||||||
---
|
---
|
||||||
|
|
||||||
# Story 65: Standardised script/test Entry Point for All Projects
|
# Story 65: Standardised script/test Entry Point for All Projects
|
||||||
|
|||||||
@@ -26,7 +26,13 @@ export type WsResponse =
|
|||||||
| { type: "update"; messages: Message[] }
|
| { type: "update"; messages: Message[] }
|
||||||
| { type: "session_id"; session_id: string }
|
| { type: "session_id"; session_id: string }
|
||||||
| { type: "error"; message: string }
|
| { type: "error"; message: string }
|
||||||
| { type: "pipeline_state"; upcoming: PipelineStageItem[]; current: PipelineStageItem[]; qa: PipelineStageItem[]; merge: PipelineStageItem[] };
|
| {
|
||||||
|
type: "pipeline_state";
|
||||||
|
upcoming: PipelineStageItem[];
|
||||||
|
current: PipelineStageItem[];
|
||||||
|
qa: PipelineStageItem[];
|
||||||
|
merge: PipelineStageItem[];
|
||||||
|
};
|
||||||
|
|
||||||
export interface ProviderConfig {
|
export interface ProviderConfig {
|
||||||
provider: string;
|
provider: string;
|
||||||
@@ -280,7 +286,13 @@ export class ChatWebSocket {
|
|||||||
if (data.type === "update") this.onUpdate?.(data.messages);
|
if (data.type === "update") this.onUpdate?.(data.messages);
|
||||||
if (data.type === "session_id") this.onSessionId?.(data.session_id);
|
if (data.type === "session_id") this.onSessionId?.(data.session_id);
|
||||||
if (data.type === "error") this.onError?.(data.message);
|
if (data.type === "error") this.onError?.(data.message);
|
||||||
if (data.type === "pipeline_state") this.onPipelineState?.({ upcoming: data.upcoming, current: data.current, qa: data.qa, merge: data.merge });
|
if (data.type === "pipeline_state")
|
||||||
|
this.onPipelineState?.({
|
||||||
|
upcoming: data.upcoming,
|
||||||
|
current: data.current,
|
||||||
|
qa: data.qa,
|
||||||
|
merge: data.merge,
|
||||||
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.onError?.(String(err));
|
this.onError?.(String(err));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -588,9 +588,7 @@ export function AgentPanel() {
|
|||||||
fontSize: "0.8em",
|
fontSize: "0.8em",
|
||||||
padding: "0 4px",
|
padding: "0 4px",
|
||||||
transform:
|
transform:
|
||||||
expandedKey === key
|
expandedKey === key ? "rotate(90deg)" : "rotate(0deg)",
|
||||||
? "rotate(90deg)"
|
|
||||||
: "rotate(0deg)",
|
|
||||||
transition: "transform 0.15s",
|
transition: "transform 0.15s",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -618,9 +616,7 @@ export function AgentPanel() {
|
|||||||
{(a.status === "running" || a.status === "pending") && (
|
{(a.status === "running" || a.status === "pending") && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() =>
|
onClick={() => handleStop(key.split(":")[0], a.agentName)}
|
||||||
handleStop(key.split(":")[0], a.agentName)
|
|
||||||
}
|
|
||||||
style={{
|
style={{
|
||||||
padding: "4px 10px",
|
padding: "4px 10px",
|
||||||
borderRadius: "999px",
|
borderRadius: "999px",
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import * as React from "react";
|
|||||||
import Markdown from "react-markdown";
|
import Markdown from "react-markdown";
|
||||||
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
|
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
|
||||||
import { oneDark } from "react-syntax-highlighter/dist/esm/styles/prism";
|
import { oneDark } from "react-syntax-highlighter/dist/esm/styles/prism";
|
||||||
import { api, ChatWebSocket } from "../api/client";
|
|
||||||
import type { PipelineState } from "../api/client";
|
import type { PipelineState } from "../api/client";
|
||||||
|
import { api, ChatWebSocket } from "../api/client";
|
||||||
import type { Message, ProviderConfig, ToolCall } from "../types";
|
import type { Message, ProviderConfig, ToolCall } from "../types";
|
||||||
import { AgentPanel } from "./AgentPanel";
|
import { AgentPanel } from "./AgentPanel";
|
||||||
import { ChatHeader } from "./ChatHeader";
|
import { ChatHeader } from "./ChatHeader";
|
||||||
@@ -719,7 +719,6 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
|
|||||||
<StagePanel title="QA" items={pipeline.qa} />
|
<StagePanel title="QA" items={pipeline.qa} />
|
||||||
<StagePanel title="Current" items={pipeline.current} />
|
<StagePanel title="Current" items={pipeline.current} />
|
||||||
<StagePanel title="Upcoming" items={pipeline.upcoming} />
|
<StagePanel title="Upcoming" items={pipeline.upcoming} />
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -43,9 +43,7 @@ export function StagePanel({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{items.length === 0 ? (
|
{items.length === 0 ? (
|
||||||
<div style={{ fontSize: "0.85em", color: "#555" }}>
|
<div style={{ fontSize: "0.85em", color: "#555" }}>{emptyMessage}</div>
|
||||||
{emptyMessage}
|
|
||||||
</div>
|
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
15
script/test
Executable file
15
script/test
Executable file
@@ -0,0 +1,15 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
|
|
||||||
|
echo "=== Running Rust tests ==="
|
||||||
|
cargo test --manifest-path "$PROJECT_ROOT/Cargo.toml"
|
||||||
|
|
||||||
|
echo "=== Running frontend unit tests ==="
|
||||||
|
cd "$PROJECT_ROOT/frontend"
|
||||||
|
pnpm test
|
||||||
|
|
||||||
|
echo "=== Running e2e tests ==="
|
||||||
|
pnpm test:e2e
|
||||||
@@ -983,38 +983,32 @@ fn check_uncommitted_changes(path: &Path) -> Result<(), String> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Run `cargo clippy` and `cargo nextest run` (falling back to `cargo test`) in
|
/// Run the project's test suite.
|
||||||
/// the given directory. Returns `(gates_passed, combined_output)`.
|
///
|
||||||
fn run_acceptance_gates(path: &Path) -> Result<(bool, String), String> {
|
/// Uses `script/test` if present, treating it as the canonical single test entry point.
|
||||||
let mut all_output = String::new();
|
/// Falls back to `cargo nextest run` / `cargo test` when `script/test` is absent.
|
||||||
let mut all_passed = true;
|
/// Returns `(tests_passed, output)`.
|
||||||
|
fn run_project_tests(path: &Path) -> Result<(bool, String), String> {
|
||||||
// ── cargo clippy ──────────────────────────────────────────────
|
let script_test = path.join("script").join("test");
|
||||||
let clippy = Command::new("cargo")
|
if script_test.exists() {
|
||||||
.args(["clippy", "--all-targets", "--all-features"])
|
let mut output = String::from("=== script/test ===\n");
|
||||||
|
let result = Command::new(&script_test)
|
||||||
.current_dir(path)
|
.current_dir(path)
|
||||||
.output()
|
.output()
|
||||||
.map_err(|e| format!("Failed to run cargo clippy: {e}"))?;
|
.map_err(|e| format!("Failed to run script/test: {e}"))?;
|
||||||
|
let out = format!(
|
||||||
all_output.push_str("=== cargo clippy ===\n");
|
"{}{}",
|
||||||
let clippy_stdout = String::from_utf8_lossy(&clippy.stdout);
|
String::from_utf8_lossy(&result.stdout),
|
||||||
let clippy_stderr = String::from_utf8_lossy(&clippy.stderr);
|
String::from_utf8_lossy(&result.stderr)
|
||||||
if !clippy_stdout.is_empty() {
|
);
|
||||||
all_output.push_str(&clippy_stdout);
|
output.push_str(&out);
|
||||||
}
|
output.push('\n');
|
||||||
if !clippy_stderr.is_empty() {
|
return Ok((result.status.success(), output));
|
||||||
all_output.push_str(&clippy_stderr);
|
|
||||||
}
|
|
||||||
all_output.push('\n');
|
|
||||||
|
|
||||||
if !clippy.status.success() {
|
|
||||||
all_passed = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── cargo nextest run (fallback: cargo test) ──────────────────
|
// Fallback: cargo nextest run / cargo test
|
||||||
all_output.push_str("=== tests ===\n");
|
let mut output = String::from("=== tests ===\n");
|
||||||
|
let (success, test_out) = match Command::new("cargo")
|
||||||
let (test_success, test_out) = match Command::new("cargo")
|
|
||||||
.args(["nextest", "run"])
|
.args(["nextest", "run"])
|
||||||
.current_dir(path)
|
.current_dir(path)
|
||||||
.output()
|
.output()
|
||||||
@@ -1042,10 +1036,43 @@ fn run_acceptance_gates(path: &Path) -> Result<(bool, String), String> {
|
|||||||
(o.status.success(), combined)
|
(o.status.success(), combined)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
output.push_str(&test_out);
|
||||||
|
output.push('\n');
|
||||||
|
Ok((success, output))
|
||||||
|
}
|
||||||
|
|
||||||
all_output.push_str(&test_out);
|
/// Run `cargo clippy` and the project test suite (via `script/test` if present,
|
||||||
|
/// otherwise `cargo nextest run` / `cargo test`) in the given directory.
|
||||||
|
/// Returns `(gates_passed, combined_output)`.
|
||||||
|
fn run_acceptance_gates(path: &Path) -> Result<(bool, String), String> {
|
||||||
|
let mut all_output = String::new();
|
||||||
|
let mut all_passed = true;
|
||||||
|
|
||||||
|
// ── cargo clippy ──────────────────────────────────────────────
|
||||||
|
let clippy = Command::new("cargo")
|
||||||
|
.args(["clippy", "--all-targets", "--all-features"])
|
||||||
|
.current_dir(path)
|
||||||
|
.output()
|
||||||
|
.map_err(|e| format!("Failed to run cargo clippy: {e}"))?;
|
||||||
|
|
||||||
|
all_output.push_str("=== cargo clippy ===\n");
|
||||||
|
let clippy_stdout = String::from_utf8_lossy(&clippy.stdout);
|
||||||
|
let clippy_stderr = String::from_utf8_lossy(&clippy.stderr);
|
||||||
|
if !clippy_stdout.is_empty() {
|
||||||
|
all_output.push_str(&clippy_stdout);
|
||||||
|
}
|
||||||
|
if !clippy_stderr.is_empty() {
|
||||||
|
all_output.push_str(&clippy_stderr);
|
||||||
|
}
|
||||||
all_output.push('\n');
|
all_output.push('\n');
|
||||||
|
|
||||||
|
if !clippy.status.success() {
|
||||||
|
all_passed = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── tests (script/test if available, else cargo nextest/test) ─
|
||||||
|
let (test_success, test_out) = run_project_tests(path)?;
|
||||||
|
all_output.push_str(&test_out);
|
||||||
if !test_success {
|
if !test_success {
|
||||||
all_passed = false;
|
all_passed = false;
|
||||||
}
|
}
|
||||||
@@ -1151,45 +1178,16 @@ fn run_merge_quality_gates(project_root: &Path) -> Result<(bool, String), String
|
|||||||
all_passed = false;
|
all_passed = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── cargo nextest run (fallback: cargo test) ──────────────────
|
// ── tests (script/test if available, else cargo nextest/test) ─
|
||||||
all_output.push_str("=== tests ===\n");
|
let (test_success, test_out) = run_project_tests(project_root)?;
|
||||||
|
|
||||||
let (test_success, test_out) = match Command::new("cargo")
|
|
||||||
.args(["nextest", "run"])
|
|
||||||
.current_dir(project_root)
|
|
||||||
.output()
|
|
||||||
{
|
|
||||||
Ok(o) => {
|
|
||||||
let combined = format!(
|
|
||||||
"{}{}",
|
|
||||||
String::from_utf8_lossy(&o.stdout),
|
|
||||||
String::from_utf8_lossy(&o.stderr)
|
|
||||||
);
|
|
||||||
(o.status.success(), combined)
|
|
||||||
}
|
|
||||||
Err(_) => {
|
|
||||||
let o = Command::new("cargo")
|
|
||||||
.args(["test"])
|
|
||||||
.current_dir(project_root)
|
|
||||||
.output()
|
|
||||||
.map_err(|e| format!("Failed to run cargo test: {e}"))?;
|
|
||||||
let combined = format!(
|
|
||||||
"{}{}",
|
|
||||||
String::from_utf8_lossy(&o.stdout),
|
|
||||||
String::from_utf8_lossy(&o.stderr)
|
|
||||||
);
|
|
||||||
(o.status.success(), combined)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
all_output.push_str(&test_out);
|
all_output.push_str(&test_out);
|
||||||
all_output.push('\n');
|
|
||||||
|
|
||||||
if !test_success {
|
if !test_success {
|
||||||
all_passed = false;
|
all_passed = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── pnpm (if frontend/ directory exists) ─────────────────────
|
// ── pnpm build (if frontend/ directory exists) ────────────────
|
||||||
|
// pnpm test is handled by script/test when present; only run it here as
|
||||||
|
// a standalone fallback when there is no script/test.
|
||||||
let frontend_dir = project_root.join("frontend");
|
let frontend_dir = project_root.join("frontend");
|
||||||
if frontend_dir.exists() {
|
if frontend_dir.exists() {
|
||||||
all_output.push_str("=== pnpm build ===\n");
|
all_output.push_str("=== pnpm build ===\n");
|
||||||
@@ -1211,6 +1209,10 @@ fn run_merge_quality_gates(project_root: &Path) -> Result<(bool, String), String
|
|||||||
all_passed = false;
|
all_passed = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only run pnpm test separately when script/test is absent (it would
|
||||||
|
// already cover frontend tests in that case).
|
||||||
|
let script_test = project_root.join("script").join("test");
|
||||||
|
if !script_test.exists() {
|
||||||
all_output.push_str("=== pnpm test ===\n");
|
all_output.push_str("=== pnpm test ===\n");
|
||||||
let pnpm_test = Command::new("pnpm")
|
let pnpm_test = Command::new("pnpm")
|
||||||
.args(["test", "--run"])
|
.args(["test", "--run"])
|
||||||
@@ -1218,18 +1220,19 @@ fn run_merge_quality_gates(project_root: &Path) -> Result<(bool, String), String
|
|||||||
.output()
|
.output()
|
||||||
.map_err(|e| format!("Failed to run pnpm test: {e}"))?;
|
.map_err(|e| format!("Failed to run pnpm test: {e}"))?;
|
||||||
|
|
||||||
let test_out = format!(
|
let pnpm_test_out = format!(
|
||||||
"{}{}",
|
"{}{}",
|
||||||
String::from_utf8_lossy(&pnpm_test.stdout),
|
String::from_utf8_lossy(&pnpm_test.stdout),
|
||||||
String::from_utf8_lossy(&pnpm_test.stderr)
|
String::from_utf8_lossy(&pnpm_test.stderr)
|
||||||
);
|
);
|
||||||
all_output.push_str(&test_out);
|
all_output.push_str(&pnpm_test_out);
|
||||||
all_output.push('\n');
|
all_output.push('\n');
|
||||||
|
|
||||||
if !pnpm_test.status.success() {
|
if !pnpm_test.status.success() {
|
||||||
all_passed = false;
|
all_passed = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok((all_passed, all_output))
|
Ok((all_passed, all_output))
|
||||||
}
|
}
|
||||||
@@ -1875,4 +1878,50 @@ mod tests {
|
|||||||
assert!(archived.exists(), "archived file should exist");
|
assert!(archived.exists(), "archived file should exist");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── run_project_tests tests ───────────────────────────────────
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
#[test]
|
||||||
|
fn run_project_tests_uses_script_test_when_present_and_passes() {
|
||||||
|
use std::fs;
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
use tempfile::tempdir;
|
||||||
|
|
||||||
|
let tmp = tempdir().unwrap();
|
||||||
|
let path = tmp.path();
|
||||||
|
let script_dir = path.join("script");
|
||||||
|
fs::create_dir_all(&script_dir).unwrap();
|
||||||
|
let script_test = script_dir.join("test");
|
||||||
|
fs::write(&script_test, "#!/usr/bin/env bash\necho 'all tests passed'\nexit 0\n").unwrap();
|
||||||
|
let mut perms = fs::metadata(&script_test).unwrap().permissions();
|
||||||
|
perms.set_mode(0o755);
|
||||||
|
fs::set_permissions(&script_test, perms).unwrap();
|
||||||
|
|
||||||
|
let (passed, output) = run_project_tests(path).unwrap();
|
||||||
|
assert!(passed, "script/test exiting 0 should pass");
|
||||||
|
assert!(output.contains("script/test"), "output should mention script/test");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
#[test]
|
||||||
|
fn run_project_tests_reports_failure_when_script_test_exits_nonzero() {
|
||||||
|
use std::fs;
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
use tempfile::tempdir;
|
||||||
|
|
||||||
|
let tmp = tempdir().unwrap();
|
||||||
|
let path = tmp.path();
|
||||||
|
let script_dir = path.join("script");
|
||||||
|
fs::create_dir_all(&script_dir).unwrap();
|
||||||
|
let script_test = script_dir.join("test");
|
||||||
|
fs::write(&script_test, "#!/usr/bin/env bash\nexit 1\n").unwrap();
|
||||||
|
let mut perms = fs::metadata(&script_test).unwrap().permissions();
|
||||||
|
perms.set_mode(0o755);
|
||||||
|
fs::set_permissions(&script_test, perms).unwrap();
|
||||||
|
|
||||||
|
let (passed, output) = run_project_tests(path).unwrap();
|
||||||
|
assert!(!passed, "script/test exiting 1 should fail");
|
||||||
|
assert!(output.contains("script/test"), "output should mention script/test");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -379,6 +379,8 @@ To support both Remote and Local models, the system implements a `ModelProvider`
|
|||||||
* Shell commands that modify state (non-readonly) should ideally require a UI confirmation (configurable).
|
* Shell commands that modify state (non-readonly) should ideally require a UI confirmation (configurable).
|
||||||
* File writes must be confirmed or revertible."#;
|
* File writes must be confirmed or revertible."#;
|
||||||
|
|
||||||
|
const STORY_KIT_SCRIPT_TEST: &str = "#!/usr/bin/env bash\nset -euo pipefail\n\n# Add your project's test commands here.\n# Story Kit agents invoke this script as the canonical test runner.\n# Exit 0 on success, non-zero on failure.\necho \"No tests configured\"\n";
|
||||||
|
|
||||||
/// Walk from `start` up through parent directories, returning the first
|
/// Walk from `start` up through parent directories, returning the first
|
||||||
/// directory that contains a `.story_kit/` subdirectory, or `None`.
|
/// directory that contains a `.story_kit/` subdirectory, or `None`.
|
||||||
pub fn find_story_kit_root(start: &Path) -> Option<PathBuf> {
|
pub fn find_story_kit_root(start: &Path) -> Option<PathBuf> {
|
||||||
@@ -468,6 +470,24 @@ fn write_file_if_missing(path: &Path, content: &str) -> Result<(), String> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Write `content` to `path` if missing, then ensure the file is executable.
|
||||||
|
fn write_script_if_missing(path: &Path, content: &str) -> Result<(), String> {
|
||||||
|
write_file_if_missing(path, content)?;
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
let mut perms = fs::metadata(path)
|
||||||
|
.map_err(|e| format!("Failed to read permissions for {}: {}", path.display(), e))?
|
||||||
|
.permissions();
|
||||||
|
perms.set_mode(0o755);
|
||||||
|
fs::set_permissions(path, perms)
|
||||||
|
.map_err(|e| format!("Failed to set permissions on {}: {}", path.display(), e))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn scaffold_story_kit(root: &Path) -> Result<(), String> {
|
fn scaffold_story_kit(root: &Path) -> Result<(), String> {
|
||||||
let story_kit_root = root.join(".story_kit");
|
let story_kit_root = root.join(".story_kit");
|
||||||
let specs_root = story_kit_root.join("specs");
|
let specs_root = story_kit_root.join("specs");
|
||||||
@@ -475,17 +495,21 @@ fn scaffold_story_kit(root: &Path) -> Result<(), String> {
|
|||||||
let functional_root = specs_root.join("functional");
|
let functional_root = specs_root.join("functional");
|
||||||
let stories_root = story_kit_root.join("stories");
|
let stories_root = story_kit_root.join("stories");
|
||||||
let archive_root = stories_root.join("archive");
|
let archive_root = stories_root.join("archive");
|
||||||
|
let script_root = root.join("script");
|
||||||
|
|
||||||
fs::create_dir_all(&tech_root).map_err(|e| format!("Failed to create specs/tech: {}", e))?;
|
fs::create_dir_all(&tech_root).map_err(|e| format!("Failed to create specs/tech: {}", e))?;
|
||||||
fs::create_dir_all(&functional_root)
|
fs::create_dir_all(&functional_root)
|
||||||
.map_err(|e| format!("Failed to create specs/functional: {}", e))?;
|
.map_err(|e| format!("Failed to create specs/functional: {}", e))?;
|
||||||
fs::create_dir_all(&archive_root)
|
fs::create_dir_all(&archive_root)
|
||||||
.map_err(|e| format!("Failed to create stories/archive: {}", e))?;
|
.map_err(|e| format!("Failed to create stories/archive: {}", e))?;
|
||||||
|
fs::create_dir_all(&script_root)
|
||||||
|
.map_err(|e| format!("Failed to create script/ directory: {}", e))?;
|
||||||
|
|
||||||
write_file_if_missing(&story_kit_root.join("README.md"), STORY_KIT_README)?;
|
write_file_if_missing(&story_kit_root.join("README.md"), STORY_KIT_README)?;
|
||||||
write_file_if_missing(&specs_root.join("README.md"), STORY_KIT_SPECS_README)?;
|
write_file_if_missing(&specs_root.join("README.md"), STORY_KIT_SPECS_README)?;
|
||||||
write_file_if_missing(&specs_root.join("00_CONTEXT.md"), STORY_KIT_CONTEXT)?;
|
write_file_if_missing(&specs_root.join("00_CONTEXT.md"), STORY_KIT_CONTEXT)?;
|
||||||
write_file_if_missing(&tech_root.join("STACK.md"), STORY_KIT_STACK)?;
|
write_file_if_missing(&tech_root.join("STACK.md"), STORY_KIT_STACK)?;
|
||||||
|
write_script_if_missing(&script_root.join("test"), STORY_KIT_SCRIPT_TEST)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -1027,6 +1051,24 @@ mod tests {
|
|||||||
assert!(dir.path().join(".story_kit/specs/00_CONTEXT.md").exists());
|
assert!(dir.path().join(".story_kit/specs/00_CONTEXT.md").exists());
|
||||||
assert!(dir.path().join(".story_kit/specs/tech/STACK.md").exists());
|
assert!(dir.path().join(".story_kit/specs/tech/STACK.md").exists());
|
||||||
assert!(dir.path().join(".story_kit/stories/archive").is_dir());
|
assert!(dir.path().join(".story_kit/stories/archive").is_dir());
|
||||||
|
assert!(dir.path().join("script/test").exists());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
#[test]
|
||||||
|
fn scaffold_story_kit_creates_executable_script_test() {
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
|
||||||
|
let dir = tempdir().unwrap();
|
||||||
|
scaffold_story_kit(dir.path()).unwrap();
|
||||||
|
|
||||||
|
let script_test = dir.path().join("script/test");
|
||||||
|
assert!(script_test.exists(), "script/test should be created");
|
||||||
|
let perms = fs::metadata(&script_test).unwrap().permissions();
|
||||||
|
assert!(
|
||||||
|
perms.mode() & 0o111 != 0,
|
||||||
|
"script/test should be executable"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
Reference in New Issue
Block a user