Skip to content
Open
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
16 changes: 5 additions & 11 deletions test/smoke-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -70,17 +70,6 @@ for skill in $SKILLS; do
check "$skill has SKILL.md.tmpl" "[ -f '$IDSTACK_DIR/skills/$skill/SKILL.md.tmpl' ]"
done

# Slugify behavior — pinned cases protect the slug derivation rule that the manifest
# schema and every skill's report-write block depend on.
check "idstack-slugify: 'Introduction to Biology 101' → introduction-to-biology-101" \
"[ \"\$('$IDSTACK_DIR/bin/idstack-slugify' 'Introduction to Biology 101')\" = 'introduction-to-biology-101' ]"
check "idstack-slugify: empty input → untitled-course" \
"[ \"\$('$IDSTACK_DIR/bin/idstack-slugify' '')\" = 'untitled-course' ]"
check "idstack-slugify: '!!!' → untitled-course" \
"[ \"\$('$IDSTACK_DIR/bin/idstack-slugify' '!!!')\" = 'untitled-course' ]"
check "idstack-slugify: unicode ASCII-folds — 'Géographie I' → geographie-i" \
"[ \"\$('$IDSTACK_DIR/bin/idstack-slugify' 'Géographie I')\" = 'geographie-i' ]"

# Skills that write per-skill HTML reports must reference the new export folder pattern
# and use the slugify helper (not the legacy .idstack/reports/<skill>.md path).
REPORT_PRODUCING_SKILLS="needs-analysis learning-objectives assessment-design course-builder course-quality-review course-import course-export accessibility-review red-team"
Expand Down Expand Up @@ -147,6 +136,11 @@ if [ -x "$IDSTACK_DIR/test/test-version-classifier.sh" ]; then
check "version-classifier unit tests pass" "'$IDSTACK_DIR/test/test-version-classifier.sh'"
fi

# Slugify (derives filesystem-safe slug from a string)
if [ -x "$IDSTACK_DIR/test/test-slugify.sh" ]; then
check "slugify unit tests pass" "'$IDSTACK_DIR/test/test-slugify.sh'"
fi

# Check generated files have auto-generated header
for skill in $SKILLS; do
check "$skill SKILL.md has auto-generated header" "grep -q 'AUTO-GENERATED from SKILL.md.tmpl' '$IDSTACK_DIR/skills/$skill/SKILL.md'"
Expand Down
104 changes: 104 additions & 0 deletions test/test-slugify.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
#!/usr/bin/env bash
# Unit tests for bin/idstack-slugify.
# Run from the repo root (or sourced by smoke-test.sh).

set -e

PASS=0
FAIL=0
TOTAL=0

REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To ensure robustness when the repository or test scripts are accessed via symbolic links, it is recommended to use pwd -P instead of pwd. This resolves the physical directory structure and maintains consistency with how IDSTACK_DIR is resolved in test/smoke-test.sh.

Suggested change
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"

SLUGIFY="$REPO_ROOT/bin/idstack-slugify"

assert() {
TOTAL=$((TOTAL + 1))
local description="$1"
local command="$2"
local expected="$3"

local got
# Run the command, safely capturing output.
if got=$(eval "$command" 2>/dev/null); then
if [ "$got" = "$expected" ]; then
PASS=$((PASS + 1))
echo " PASS: $description -> $got"
else
FAIL=$((FAIL + 1))
echo " FAIL: $description -> $got (expected: $expected)"
fi
else
FAIL=$((FAIL + 1))
echo " FAIL: $description (command failed unexpectedly)"
fi
}

assert_exit_code() {
TOTAL=$((TOTAL + 1))
local description="$1"
local command="$2"
local expected_code="$3"

set +e
eval "$command" >/dev/null 2>&1
local code=$?
set -e
Comment on lines +42 to +45

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Toggling set -e and set +e globally within a function can be risky if the function exits prematurely or if there are other side effects. Instead, you can safely capture the exit code of the command without triggering set -e by executing it as part of an || (OR) list.

Suggested change
set +e
eval "$command" >/dev/null 2>&1
local code=$?
set -e
local code=0
eval "$command" >/dev/null 2>&1 || code=$?


if [ "$code" -eq "$expected_code" ]; then
PASS=$((PASS + 1))
echo " PASS: $description -> exit code $code"
else
FAIL=$((FAIL + 1))
echo " FAIL: $description -> exit code $code (expected: $expected_code)"
fi
}


if [ ! -x "$SLUGIFY" ]; then
echo "test-slugify: $SLUGIFY missing or not executable"
exit 1
fi

echo "test-slugify"
echo ""

# --- Happy path: standard strings ---
assert "standard string" "'$SLUGIFY' 'Introduction to Biology 101'" "introduction-to-biology-101"
assert "lowercase stays lowercase" "'$SLUGIFY' 'hello world'" "hello-world"
assert "uppercase becomes lowercase" "'$SLUGIFY' 'HELLO WORLD'" "hello-world"

# --- Edge cases: special characters and hyphens ---
assert "multiple spaces become single hyphen" "'$SLUGIFY' 'hello world'" "hello-world"
assert "special characters become hyphens" "'$SLUGIFY' 'hello!@#world'" "hello-world"
assert "leading hyphens are stripped" "'$SLUGIFY' '---hello'" "hello"
assert "trailing hyphens are stripped" "'$SLUGIFY' 'world---'" "world"
assert "mixed hyphens and spaces" "'$SLUGIFY' ' -hello- world- '" "hello-world"
assert "all special characters" "'$SLUGIFY' '!@#$%^&*()'" "untitled-course"
assert "only hyphens" "'$SLUGIFY' '---'" "untitled-course"
assert "only spaces" "'$SLUGIFY' ' '" "untitled-course"

# --- Edge case: empty input ---
assert "empty string" "'$SLUGIFY' ''" "untitled-course"

# --- Unicode and accents (NFKD folding) ---
assert "unicode folding (é, I)" "'$SLUGIFY' 'Géographie I'" "geographie-i"
assert "unicode characters" "'$SLUGIFY' 'Über den Wolken'" "uber-den-wolken"
assert "non-ascii stripped" "'$SLUGIFY' 'Hello 🌍 World'" "hello-world"

# --- Input methods ---
assert "stdin implicitly" "echo 'Pipe Test' | '$SLUGIFY'" "pipe-test"
assert "stdin explicitly with hyphen" "echo 'Explicit Pipe' | '$SLUGIFY' -" "explicit-pipe"

# --- Error cases ---
# Test exit code 3 when no argument and no stdin
# The tool checks [ -t 0 ] to detect if it's connected to a terminal.
# We simulate a terminal using python's pty module.
if command -v python3 >/dev/null 2>&1; then
assert_exit_code "no input given (exit 3)" "python3 -c 'import sys, os, pty, subprocess; pid, fd = pty.fork(); sys.exit(subprocess.call([\"$SLUGIFY\"])) if pid == 0 else sys.exit(os.waitstatus_to_exitcode(os.waitpid(pid, 0)[1]))'" 3

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The python command uses os.waitstatus_to_exitcode, which was introduced in Python 3.9. This will cause the test suite to fail with an AttributeError on systems running older Python versions (such as Python 3.8, which is the default on Ubuntu 20.04 LTS).

Using os.execv and os.WEXITSTATUS is fully compatible with all Python 3 versions and is cleaner.

Suggested change
assert_exit_code "no input given (exit 3)" "python3 -c 'import sys, os, pty, subprocess; pid, fd = pty.fork(); sys.exit(subprocess.call([\"$SLUGIFY\"])) if pid == 0 else sys.exit(os.waitstatus_to_exitcode(os.waitpid(pid, 0)[1]))'" 3
assert_exit_code "no input given (exit 3)" "python3 -c 'import os, pty, sys; pid, fd = pty.fork(); os.execv(\"$SLUGIFY\", [\"$SLUGIFY\"]) if pid == 0 else sys.exit(os.WEXITSTATUS(os.waitpid(pid, 0)[1]))'" 3

else
echo " SKIP: no input given (exit 3) — python3 required for pty simulation"
fi

echo ""
echo "slugify: $PASS/$TOTAL passed, $FAIL failed"
[ "$FAIL" -eq 0 ] && exit 0 || exit 1