mirror of https://github.com/garrytan/gstack.git
test: add zsh glob safety test + fix 2 missed resolver globs
Adds a test that scans all generated SKILL.md bash blocks for raw glob patterns and verifies they have either a find-based replacement or a setopt +o nomatch guard. The test immediately caught 2 unguarded blocks in review.ts (design doc re-check and plan file discovery). Also syncs package.json version to 0.12.8.1. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
4fc43c4e72
commit
8e8f3afcae
|
|
@ -408,6 +408,7 @@ If the Read fails (file not found), say:
|
||||||
|
|
||||||
After /office-hours completes, re-run the design doc check:
|
After /office-hours completes, re-run the design doc check:
|
||||||
```bash
|
```bash
|
||||||
|
setopt +o nomatch 2>/dev/null || true # zsh compat
|
||||||
SLUG=$(~/.claude/skills/gstack/browse/bin/remote-slug 2>/dev/null || basename "$(git rev-parse --show-toplevel 2>/dev/null || pwd)")
|
SLUG=$(~/.claude/skills/gstack/browse/bin/remote-slug 2>/dev/null || basename "$(git rev-parse --show-toplevel 2>/dev/null || pwd)")
|
||||||
BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null | tr '/' '-' || echo 'no-branch')
|
BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null | tr '/' '-' || echo 'no-branch')
|
||||||
DESIGN=$(ls -t ~/.gstack/projects/$SLUG/*-$BRANCH-design-*.md 2>/dev/null | head -1)
|
DESIGN=$(ls -t ~/.gstack/projects/$SLUG/*-$BRANCH-design-*.md 2>/dev/null | head -1)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "gstack",
|
"name": "gstack",
|
||||||
"version": "0.12.8.0",
|
"version": "0.12.8.1",
|
||||||
"description": "Garry's Stack — Claude Code skills + fast headless browser. One repo, one install, entire AI engineering workflow.",
|
"description": "Garry's Stack — Claude Code skills + fast headless browser. One repo, one install, entire AI engineering workflow.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|
|
||||||
|
|
@ -511,6 +511,7 @@ If the Read fails (file not found), say:
|
||||||
|
|
||||||
After /office-hours completes, re-run the design doc check:
|
After /office-hours completes, re-run the design doc check:
|
||||||
```bash
|
```bash
|
||||||
|
setopt +o nomatch 2>/dev/null || true # zsh compat
|
||||||
SLUG=$(~/.claude/skills/gstack/browse/bin/remote-slug 2>/dev/null || basename "$(git rev-parse --show-toplevel 2>/dev/null || pwd)")
|
SLUG=$(~/.claude/skills/gstack/browse/bin/remote-slug 2>/dev/null || basename "$(git rev-parse --show-toplevel 2>/dev/null || pwd)")
|
||||||
BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null | tr '/' '-' || echo 'no-branch')
|
BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null | tr '/' '-' || echo 'no-branch')
|
||||||
DESIGN=$(ls -t ~/.gstack/projects/$SLUG/*-$BRANCH-design-*.md 2>/dev/null | head -1)
|
DESIGN=$(ls -t ~/.gstack/projects/$SLUG/*-$BRANCH-design-*.md 2>/dev/null | head -1)
|
||||||
|
|
|
||||||
|
|
@ -421,6 +421,7 @@ If the Read fails (file not found), say:
|
||||||
|
|
||||||
After /office-hours completes, re-run the design doc check:
|
After /office-hours completes, re-run the design doc check:
|
||||||
```bash
|
```bash
|
||||||
|
setopt +o nomatch 2>/dev/null || true # zsh compat
|
||||||
SLUG=$(~/.claude/skills/gstack/browse/bin/remote-slug 2>/dev/null || basename "$(git rev-parse --show-toplevel 2>/dev/null || pwd)")
|
SLUG=$(~/.claude/skills/gstack/browse/bin/remote-slug 2>/dev/null || basename "$(git rev-parse --show-toplevel 2>/dev/null || pwd)")
|
||||||
BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null | tr '/' '-' || echo 'no-branch')
|
BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null | tr '/' '-' || echo 'no-branch')
|
||||||
DESIGN=$(ls -t ~/.gstack/projects/$SLUG/*-$BRANCH-design-*.md 2>/dev/null | head -1)
|
DESIGN=$(ls -t ~/.gstack/projects/$SLUG/*-$BRANCH-design-*.md 2>/dev/null | head -1)
|
||||||
|
|
|
||||||
|
|
@ -394,6 +394,7 @@ Before reviewing code quality, check: **did they build what was requested — no
|
||||||
2. **Content-based search (fallback):** If no plan file is referenced in conversation context, search by content:
|
2. **Content-based search (fallback):** If no plan file is referenced in conversation context, search by content:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
setopt +o nomatch 2>/dev/null || true # zsh compat
|
||||||
BRANCH=$(git branch --show-current 2>/dev/null | tr '/' '-')
|
BRANCH=$(git branch --show-current 2>/dev/null | tr '/' '-')
|
||||||
REPO=$(basename "$(git rev-parse --show-toplevel 2>/dev/null)")
|
REPO=$(basename "$(git rev-parse --show-toplevel 2>/dev/null)")
|
||||||
# Search common plan file locations
|
# Search common plan file locations
|
||||||
|
|
|
||||||
|
|
@ -233,6 +233,7 @@ If the Read fails (file not found), say:
|
||||||
|
|
||||||
After /${first} completes, re-run the design doc check:
|
After /${first} completes, re-run the design doc check:
|
||||||
\`\`\`bash
|
\`\`\`bash
|
||||||
|
setopt +o nomatch 2>/dev/null || true # zsh compat
|
||||||
SLUG=$(~/.claude/skills/gstack/browse/bin/remote-slug 2>/dev/null || basename "$(git rev-parse --show-toplevel 2>/dev/null || pwd)")
|
SLUG=$(~/.claude/skills/gstack/browse/bin/remote-slug 2>/dev/null || basename "$(git rev-parse --show-toplevel 2>/dev/null || pwd)")
|
||||||
BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null | tr '/' '-' || echo 'no-branch')
|
BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null | tr '/' '-' || echo 'no-branch')
|
||||||
DESIGN=$(ls -t ~/.gstack/projects/$SLUG/*-$BRANCH-design-*.md 2>/dev/null | head -1)
|
DESIGN=$(ls -t ~/.gstack/projects/$SLUG/*-$BRANCH-design-*.md 2>/dev/null | head -1)
|
||||||
|
|
@ -614,6 +615,7 @@ function generatePlanFileDiscovery(): string {
|
||||||
2. **Content-based search (fallback):** If no plan file is referenced in conversation context, search by content:
|
2. **Content-based search (fallback):** If no plan file is referenced in conversation context, search by content:
|
||||||
|
|
||||||
\`\`\`bash
|
\`\`\`bash
|
||||||
|
setopt +o nomatch 2>/dev/null || true # zsh compat
|
||||||
BRANCH=$(git branch --show-current 2>/dev/null | tr '/' '-')
|
BRANCH=$(git branch --show-current 2>/dev/null | tr '/' '-')
|
||||||
REPO=$(basename "$(git rev-parse --show-toplevel 2>/dev/null)")
|
REPO=$(basename "$(git rev-parse --show-toplevel 2>/dev/null)")
|
||||||
# Search common plan file locations
|
# Search common plan file locations
|
||||||
|
|
|
||||||
|
|
@ -1126,6 +1126,7 @@ Repo: {owner/repo}
|
||||||
2. **Content-based search (fallback):** If no plan file is referenced in conversation context, search by content:
|
2. **Content-based search (fallback):** If no plan file is referenced in conversation context, search by content:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
setopt +o nomatch 2>/dev/null || true # zsh compat
|
||||||
BRANCH=$(git branch --show-current 2>/dev/null | tr '/' '-')
|
BRANCH=$(git branch --show-current 2>/dev/null | tr '/' '-')
|
||||||
REPO=$(basename "$(git rev-parse --show-toplevel 2>/dev/null)")
|
REPO=$(basename "$(git rev-parse --show-toplevel 2>/dev/null)")
|
||||||
# Search common plan file locations
|
# Search common plan file locations
|
||||||
|
|
|
||||||
|
|
@ -263,6 +263,43 @@ describe('gen-skill-docs', () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('bash blocks with shell globs are zsh-safe (setopt guard or find)', () => {
|
||||||
|
for (const skill of ALL_SKILLS) {
|
||||||
|
const content = fs.readFileSync(path.join(ROOT, skill.dir, 'SKILL.md'), 'utf-8');
|
||||||
|
const bashBlocks = [...content.matchAll(/```bash\n([\s\S]*?)```/g)].map(m => m[1]);
|
||||||
|
|
||||||
|
for (const block of bashBlocks) {
|
||||||
|
const lines = block.split('\n');
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const trimmed = line.trimStart();
|
||||||
|
if (trimmed.startsWith('#')) continue;
|
||||||
|
if (!trimmed.includes('*')) continue;
|
||||||
|
// Skip lines where * is inside find -name, git pathspecs, or $(find)
|
||||||
|
if (/\bfind\b/.test(trimmed)) continue;
|
||||||
|
if (/\bgit\b/.test(trimmed)) continue;
|
||||||
|
if (/\$\(find\b/.test(trimmed)) continue;
|
||||||
|
|
||||||
|
// Check 1: "for VAR in <glob>" must use $(find ...) — caught above by the
|
||||||
|
// $(find check, so any surviving for-in with a glob pattern is a violation
|
||||||
|
if (/\bfor\s+\w+\s+in\b/.test(trimmed) && /\*\./.test(trimmed)) {
|
||||||
|
throw new Error(
|
||||||
|
`Unsafe for-in glob in ${skill.dir}/SKILL.md: "${trimmed}". ` +
|
||||||
|
`Use \`for f in $(find ... -name '*.ext')\` for zsh compatibility.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check 2: ls/cat/rm/grep with glob file args must have setopt guard
|
||||||
|
const isGlobCmd = /\b(?:ls|cat|rm|grep)\b/.test(trimmed) &&
|
||||||
|
/(?:\/\*[a-z.*]|\*\.[a-z])/.test(trimmed);
|
||||||
|
if (isGlobCmd) {
|
||||||
|
expect(block).toContain('setopt +o nomatch');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test('preamble-using skills have correct skill name in telemetry', () => {
|
test('preamble-using skills have correct skill name in telemetry', () => {
|
||||||
const PREAMBLE_SKILLS = [
|
const PREAMBLE_SKILLS = [
|
||||||
{ dir: '.', name: 'gstack' },
|
{ dir: '.', name: 'gstack' },
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue