mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-05 05:05:08 +02:00
merge: incorporate origin/main into community-mode branch
Conflicts resolved: - VERSION: keep 0.13.0.0 (branch > main's 0.12.12.0) - package.json: same version resolution - CHANGELOG.md: keep both entries — 0.13.0.0 on top, then 0.12.12.0/11.0/10.0 - scripts/gen-skill-docs.ts: keep resolvers-based architecture, drop main's inline Codex helper duplicates (already in scripts/resolvers/codex-helpers.ts) Main brought in: security audit compliance (conditional telemetry, credential cleanup, dead code removal), skill prefix choice, Codex filesystem boundary, audit regression tests. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
import { describe, test, expect } from 'bun:test';
|
||||
import { readFileSync, readdirSync, existsSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
|
||||
const ROOT = join(import.meta.dir, '..');
|
||||
|
||||
function getAllSkillMds(): Array<{ name: string; content: string }> {
|
||||
const results: Array<{ name: string; content: string }> = [];
|
||||
const rootPath = join(ROOT, 'SKILL.md');
|
||||
if (existsSync(rootPath)) {
|
||||
results.push({ name: 'root', content: readFileSync(rootPath, 'utf-8') });
|
||||
}
|
||||
for (const entry of readdirSync(ROOT, { withFileTypes: true })) {
|
||||
if (!entry.isDirectory() || entry.name.startsWith('.') || entry.name === 'node_modules') continue;
|
||||
const skillPath = join(ROOT, entry.name, 'SKILL.md');
|
||||
if (existsSync(skillPath)) {
|
||||
results.push({ name: entry.name, content: readFileSync(skillPath, 'utf-8') });
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
describe('Audit compliance', () => {
|
||||
// Fix 1: W007 — No hardcoded credentials in documentation
|
||||
test('no hardcoded credential patterns in SKILL.md.tmpl', () => {
|
||||
const tmpl = readFileSync(join(ROOT, 'SKILL.md.tmpl'), 'utf-8');
|
||||
expect(tmpl).not.toContain('"password123"');
|
||||
expect(tmpl).not.toContain('"test@example.com"');
|
||||
expect(tmpl).not.toContain('"test@test.com"');
|
||||
expect(tmpl).toContain('$TEST_EMAIL');
|
||||
expect(tmpl).toContain('$TEST_PASSWORD');
|
||||
});
|
||||
|
||||
// Fix 2: Conditional telemetry — binary calls wrapped with existence check
|
||||
test('preamble telemetry calls are conditional on _TEL and binary existence', () => {
|
||||
const preamble = readFileSync(join(ROOT, 'scripts/resolvers/preamble.ts'), 'utf-8');
|
||||
// Pending finalization must check _TEL and binary existence
|
||||
expect(preamble).toContain('_TEL" != "off"');
|
||||
expect(preamble).toContain('-x ');
|
||||
expect(preamble).toContain('gstack-telemetry-log');
|
||||
// End-of-skill telemetry must also be conditional
|
||||
const completionIdx = preamble.indexOf('Telemetry (run last)');
|
||||
expect(completionIdx).toBeGreaterThan(-1);
|
||||
const completionSection = preamble.slice(completionIdx);
|
||||
expect(completionSection).toContain('_TEL" != "off"');
|
||||
});
|
||||
|
||||
// Fix 3: W012 — Bun install is version-pinned
|
||||
test('bun install commands use version pinning', () => {
|
||||
const browseResolver = readFileSync(join(ROOT, 'scripts/resolvers/browse.ts'), 'utf-8');
|
||||
expect(browseResolver).toContain('BUN_VERSION');
|
||||
// Should not have unpinned curl|bash (without BUN_VERSION on same line)
|
||||
const lines = browseResolver.split('\n');
|
||||
for (const line of lines) {
|
||||
if (line.includes('bun.sh/install') && line.includes('bash') && !line.includes('BUN_VERSION') && !line.includes('command -v')) {
|
||||
throw new Error(`Unpinned bun install found: ${line.trim()}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Fix 4: W011 — Untrusted content warning in command reference
|
||||
test('command reference includes untrusted content warning after Navigation', () => {
|
||||
const rootSkill = readFileSync(join(ROOT, 'SKILL.md'), 'utf-8');
|
||||
const navIdx = rootSkill.indexOf('### Navigation');
|
||||
const readingIdx = rootSkill.indexOf('### Reading');
|
||||
expect(navIdx).toBeGreaterThan(-1);
|
||||
expect(readingIdx).toBeGreaterThan(navIdx);
|
||||
const between = rootSkill.slice(navIdx, readingIdx);
|
||||
expect(between.toLowerCase()).toContain('untrusted');
|
||||
});
|
||||
|
||||
// Fix 5: Data flow documentation in review.ts
|
||||
test('review.ts has data flow documentation', () => {
|
||||
const review = readFileSync(join(ROOT, 'scripts/resolvers/review.ts'), 'utf-8');
|
||||
expect(review).toContain('Data sent');
|
||||
expect(review).toContain('Data NOT sent');
|
||||
});
|
||||
|
||||
// Fix 2+6: All generated SKILL.md files with telemetry are conditional
|
||||
test('all generated SKILL.md files with telemetry calls use conditional pattern', () => {
|
||||
const skills = getAllSkillMds();
|
||||
for (const { name, content } of skills) {
|
||||
if (content.includes('gstack-telemetry-log')) {
|
||||
expect(content).toContain('_TEL" != "off"');
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1058,6 +1058,67 @@ describe('CODEX_SECOND_OPINION resolver', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// --- Codex filesystem boundary tests ---
|
||||
|
||||
describe('Codex filesystem boundary', () => {
|
||||
// Skills that call codex exec/review and should contain boundary text
|
||||
const CODEX_CALLING_SKILLS = [
|
||||
'codex', // /codex skill — 3 modes
|
||||
'autoplan', // /autoplan — CEO/design/eng voices
|
||||
'review', // /review — adversarial step resolver
|
||||
'ship', // /ship — adversarial step resolver
|
||||
'plan-eng-review', // outside voice resolver
|
||||
'plan-ceo-review', // outside voice resolver
|
||||
'office-hours', // second opinion resolver
|
||||
];
|
||||
|
||||
const BOUNDARY_MARKER = 'Do NOT read or execute any';
|
||||
|
||||
test('boundary instruction appears in all skills that call codex', () => {
|
||||
for (const skill of CODEX_CALLING_SKILLS) {
|
||||
const content = fs.readFileSync(path.join(ROOT, skill, 'SKILL.md'), 'utf-8');
|
||||
expect(content).toContain(BOUNDARY_MARKER);
|
||||
}
|
||||
});
|
||||
|
||||
test('codex skill has Filesystem Boundary section', () => {
|
||||
const content = fs.readFileSync(path.join(ROOT, 'codex', 'SKILL.md'), 'utf-8');
|
||||
expect(content).toContain('## Filesystem Boundary');
|
||||
expect(content).toContain('skill definitions meant for a different AI system');
|
||||
});
|
||||
|
||||
test('codex skill has rabbit-hole detection rule', () => {
|
||||
const content = fs.readFileSync(path.join(ROOT, 'codex', 'SKILL.md'), 'utf-8');
|
||||
expect(content).toContain('Detect skill-file rabbit holes');
|
||||
expect(content).toContain('gstack-update-check');
|
||||
expect(content).toContain('Consider retrying');
|
||||
});
|
||||
|
||||
test('review.ts CODEX_BOUNDARY constant is interpolated into resolver output', () => {
|
||||
// The adversarial step resolver should include boundary text in codex exec prompts
|
||||
const reviewContent = fs.readFileSync(path.join(ROOT, 'review', 'SKILL.md'), 'utf-8');
|
||||
// Boundary should appear near codex exec invocations
|
||||
const boundaryIdx = reviewContent.indexOf(BOUNDARY_MARKER);
|
||||
const codexExecIdx = reviewContent.indexOf('codex exec');
|
||||
// Both must exist and boundary must come before a codex exec call
|
||||
expect(boundaryIdx).toBeGreaterThan(-1);
|
||||
expect(codexExecIdx).toBeGreaterThan(-1);
|
||||
});
|
||||
|
||||
test('autoplan boundary text avoids host-specific paths for cross-host compatibility', () => {
|
||||
const content = fs.readFileSync(path.join(ROOT, 'autoplan', 'SKILL.md.tmpl'), 'utf-8');
|
||||
// autoplan template uses generic 'skills/gstack' pattern instead of host-specific
|
||||
// paths like ~/.claude/ or .agents/skills (which break Codex/Claude output tests)
|
||||
const boundaryStart = content.indexOf('Filesystem Boundary');
|
||||
const boundaryEnd = content.indexOf('---', boundaryStart + 1);
|
||||
const boundarySection = content.slice(boundaryStart, boundaryEnd);
|
||||
expect(boundarySection).not.toContain('~/.claude/');
|
||||
expect(boundarySection).not.toContain('.agents/skills');
|
||||
expect(boundarySection).toContain('skills/gstack');
|
||||
expect(boundarySection).toContain(BOUNDARY_MARKER);
|
||||
});
|
||||
});
|
||||
|
||||
// --- {{BENEFITS_FROM}} resolver tests ---
|
||||
|
||||
describe('BENEFITS_FROM resolver', () => {
|
||||
@@ -1685,6 +1746,56 @@ describe('setup script validation', () => {
|
||||
);
|
||||
expect(claudeInstallSection).toContain('cleanup_old_claude_symlinks');
|
||||
});
|
||||
|
||||
// --- Persistent config + interactive prompt tests ---
|
||||
|
||||
test('setup reads skill_prefix from config', () => {
|
||||
expect(setupContent).toContain('get skill_prefix');
|
||||
expect(setupContent).toContain('GSTACK_CONFIG');
|
||||
});
|
||||
|
||||
test('setup supports --prefix flag', () => {
|
||||
expect(setupContent).toContain('--prefix)');
|
||||
expect(setupContent).toContain('SKILL_PREFIX=1; SKILL_PREFIX_FLAG=1');
|
||||
});
|
||||
|
||||
test('--prefix and --no-prefix persist to config', () => {
|
||||
expect(setupContent).toContain('set skill_prefix');
|
||||
});
|
||||
|
||||
test('interactive prompt shows when no config', () => {
|
||||
expect(setupContent).toContain('Short names');
|
||||
expect(setupContent).toContain('Namespaced');
|
||||
expect(setupContent).toContain('Choice [1/2]');
|
||||
});
|
||||
|
||||
test('non-TTY defaults to flat names', () => {
|
||||
// Should check if stdin is a TTY before prompting
|
||||
expect(setupContent).toContain('-t 0');
|
||||
});
|
||||
|
||||
test('cleanup_prefixed_claude_symlinks exists and uses readlink', () => {
|
||||
expect(setupContent).toContain('cleanup_prefixed_claude_symlinks');
|
||||
const fnStart = setupContent.indexOf('cleanup_prefixed_claude_symlinks()');
|
||||
const fnEnd = setupContent.indexOf('}', setupContent.indexOf('removed[@]}', fnStart));
|
||||
const fnBody = setupContent.slice(fnStart, fnEnd);
|
||||
expect(fnBody).toContain('readlink');
|
||||
expect(fnBody).toContain('gstack-$skill_name');
|
||||
});
|
||||
|
||||
test('reverse cleanup runs before link when prefix is disabled', () => {
|
||||
const claudeInstallSection = setupContent.slice(
|
||||
setupContent.indexOf('INSTALL_CLAUDE'),
|
||||
setupContent.lastIndexOf('link_claude_skill_dirs')
|
||||
);
|
||||
expect(claudeInstallSection).toContain('cleanup_prefixed_claude_symlinks');
|
||||
});
|
||||
|
||||
test('welcome message references SKILL_PREFIX', () => {
|
||||
// gstack-upgrade is always called gstack-upgrade (it's the actual dir name)
|
||||
// but the welcome section should exist near the prefix logic
|
||||
expect(setupContent).toContain('Run /gstack-upgrade anytime');
|
||||
});
|
||||
});
|
||||
|
||||
describe('discover-skills hidden directory filtering', () => {
|
||||
|
||||
Reference in New Issue
Block a user