mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-01 14:34:42 +01:00
fix(setup): bin/dev-setup succeeds in a fresh gstack clone (GST-768)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
a2ce5bd888
commit
24af2ff139
2 files changed
+78
-2
No files matched your search
@@ -600,10 +600,15 @@ if [ "$CODEX_REPO_LOCAL" -eq 1 ]; then
|
||||
"$_codex_project"|"$_codex_project"/*) ;;
|
||||
*) echo "Refusing: project-local Codex destination resolves outside the project: $CODEX_SKILLS -> $_codex_parent. Use a separate project-local skills directory; setup will not relink it automatically." >&2; exit 1 ;;
|
||||
esac
|
||||
_codex_self_host=0
|
||||
if [ "$_codex_project" = "$SOURCE_GSTACK_DIR" ] && [ "$_codex_parent" = "$SOURCE_GSTACK_DIR/.agents/skills" ]; then
|
||||
_codex_self_host=1
|
||||
fi
|
||||
_codex_payload="$_codex_parent"
|
||||
while :; do
|
||||
if [ "$_codex_payload" = "$SOURCE_GSTACK_DIR" ] \
|
||||
|| { [ -f "$_codex_payload/setup" ] && [ -f "$_codex_payload/VERSION" ] && [ -f "$_codex_payload/bin/gstack-relink" ]; }; then
|
||||
if { [ "$_codex_payload" = "$SOURCE_GSTACK_DIR" ] \
|
||||
|| { [ -f "$_codex_payload/setup" ] && [ -f "$_codex_payload/VERSION" ] && [ -f "$_codex_payload/bin/gstack-relink" ]; }; } \
|
||||
&& ! { [ "$_codex_self_host" -eq 1 ] && [ "$_codex_payload" = "$SOURCE_GSTACK_DIR" ]; }; then
|
||||
echo "Refusing: project-local Codex destination overlaps the source tree at $_codex_payload: $CODEX_SKILLS -> $_codex_parent. Use a separate project-local skills directory; setup will not relink it automatically." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import { afterAll, afterEach, describe, expect, test } from 'bun:test';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { readlinkSync, symlinkSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { cleanupFixtures, cleanupSeed, fixtureMkdirSync, makeFixture, makeSource, put, tree } from './helpers/install-fixture';
|
||||
|
||||
afterEach(cleanupFixtures);
|
||||
afterAll(cleanupSeed);
|
||||
|
||||
const stubBun = (f: ReturnType<typeof makeFixture>) => put(join(f.commands, 'bun'), `#!/usr/bin/env bash
|
||||
case "$*" in
|
||||
install|'install --production --frozen-lockfile') exit 0 ;;
|
||||
'build --help') echo 'Fixture Bun has no CSO compile flags'; exit 0 ;;
|
||||
'run build') echo 'Unexpected build in a dev-setup fixture' >&2; exit 90 ;;
|
||||
*) exec '${process.execPath}' "$@" ;;
|
||||
esac
|
||||
`, 0o755);
|
||||
|
||||
const freshClone = (rel = 'gstack') => {
|
||||
const f = makeFixture();
|
||||
stubBun(f);
|
||||
const source = makeSource(f, join(f.dir, rel));
|
||||
const init = spawnSync('git', ['init', '-q', source], { encoding: 'utf8', timeout: 30_000 });
|
||||
if (init.status !== 0) throw new Error(init.stderr);
|
||||
return { f, source };
|
||||
};
|
||||
|
||||
describe.skipIf(process.platform === 'win32')('bin/dev-setup in a fresh gstack clone', () => {
|
||||
test('links gstack into its own .claude and .agents skills and exits 0', () => {
|
||||
const { f, source } = freshClone();
|
||||
for (let run = 0; run < 2; run++) {
|
||||
const result = spawnSync('bash', [join(source, 'bin/dev-setup')], { cwd: source, env: f.env, encoding: 'utf8', timeout: 120_000, input: '' });
|
||||
expect(result.stderr).not.toContain('overlaps the source tree');
|
||||
expect(result.status, result.stdout + result.stderr).toBe(0);
|
||||
expect(readlinkSync(join(source, '.claude/skills/gstack'))).toBe(source);
|
||||
expect(readlinkSync(join(source, '.agents/skills/gstack'))).toBe(source);
|
||||
}
|
||||
}, 240_000);
|
||||
|
||||
test('a user project whose .agents/skills resolves to the checkout\'s own .agents/skills is still refused', () => {
|
||||
const { f, source } = freshClone('project/vendor/gstack');
|
||||
const project = join(f.dir, 'project');
|
||||
fixtureMkdirSync(join(source, '.agents/skills'), { recursive: true });
|
||||
fixtureMkdirSync(join(project, '.claude/skills'), { recursive: true });
|
||||
fixtureMkdirSync(join(project, '.agents'));
|
||||
symlinkSync(source, join(project, '.claude/skills/gstack'), 'dir');
|
||||
symlinkSync(join(source, '.agents/skills'), join(project, '.agents/skills'), 'dir');
|
||||
const before = tree(f.dir);
|
||||
const result = spawnSync('bash', [join(project, '.claude/skills/gstack/setup'), '--host', 'codex', '--no-team', '--no-plan-tune-hooks', '--no-timeline-stop-hook'], {
|
||||
cwd: project, env: f.env, encoding: 'utf8', timeout: 60_000, input: '',
|
||||
});
|
||||
expect(result.status, result.stdout + result.stderr).toBe(1);
|
||||
expect(result.stderr).toContain(`overlaps the source tree at ${source}`);
|
||||
expect(tree(f.dir)).toEqual(before);
|
||||
}, 90_000);
|
||||
|
||||
test('the checkout itself is still refused when its .agents/skills aliases the source root', () => {
|
||||
const { f, source } = freshClone();
|
||||
fixtureMkdirSync(join(source, '.claude/skills'), { recursive: true });
|
||||
fixtureMkdirSync(join(source, '.agents'));
|
||||
symlinkSync(source, join(source, '.claude/skills/gstack'), 'dir');
|
||||
symlinkSync(source, join(source, '.agents/skills'), 'dir');
|
||||
const before = tree(f.dir);
|
||||
const result = spawnSync('bash', [join(source, '.claude/skills/gstack/setup'), '--host', 'codex', '--no-team', '--no-plan-tune-hooks', '--no-timeline-stop-hook'], {
|
||||
cwd: source, env: f.env, encoding: 'utf8', timeout: 60_000, input: '',
|
||||
});
|
||||
expect(result.status, result.stdout + result.stderr).toBe(1);
|
||||
expect(result.stderr).toContain('overlaps the source tree');
|
||||
expect(tree(f.dir)).toEqual(before);
|
||||
}, 90_000);
|
||||
});
|
||||
Reference in new issue
Block a user