mirror of https://github.com/garrytan/gstack.git
Merge f412cf13ee into a3259400a3
This commit is contained in:
commit
02a312da79
15
setup
15
setup
|
|
@ -748,8 +748,9 @@ create_agents_sidecar() {
|
|||
local agents_gstack="$repo_root/.agents/skills/gstack"
|
||||
mkdir -p "$agents_gstack"
|
||||
|
||||
# Sidecar directories that skills reference at runtime
|
||||
for asset in bin browse review qa; do
|
||||
# Sidecar directories that skills reference at runtime. bin scripts import
|
||||
# shared modules via ../lib, so bin and lib must always travel together.
|
||||
for asset in bin lib browse review qa; do
|
||||
local src="$SOURCE_GSTACK_DIR/$asset"
|
||||
local dst="$agents_gstack/$asset"
|
||||
if [ -d "$src" ] || [ -f "$src" ]; then
|
||||
|
|
@ -796,6 +797,9 @@ create_codex_runtime_root() {
|
|||
if [ -d "$gstack_dir/bin" ]; then
|
||||
_link_or_copy "$gstack_dir/bin" "$codex_gstack/bin"
|
||||
fi
|
||||
if [ -d "$gstack_dir/lib" ]; then
|
||||
_link_or_copy "$gstack_dir/lib" "$codex_gstack/lib"
|
||||
fi
|
||||
if [ -d "$gstack_dir/browse/dist" ]; then
|
||||
_link_or_copy "$gstack_dir/browse/dist" "$codex_gstack/browse/dist"
|
||||
fi
|
||||
|
|
@ -836,6 +840,9 @@ create_factory_runtime_root() {
|
|||
if [ -d "$gstack_dir/bin" ]; then
|
||||
_link_or_copy "$gstack_dir/bin" "$factory_gstack/bin"
|
||||
fi
|
||||
if [ -d "$gstack_dir/lib" ]; then
|
||||
_link_or_copy "$gstack_dir/lib" "$factory_gstack/lib"
|
||||
fi
|
||||
if [ -d "$gstack_dir/browse/dist" ]; then
|
||||
_link_or_copy "$gstack_dir/browse/dist" "$factory_gstack/browse/dist"
|
||||
fi
|
||||
|
|
@ -874,6 +881,9 @@ create_opencode_runtime_root() {
|
|||
if [ -d "$gstack_dir/bin" ]; then
|
||||
_link_or_copy "$gstack_dir/bin" "$opencode_gstack/bin"
|
||||
fi
|
||||
if [ -d "$gstack_dir/lib" ]; then
|
||||
_link_or_copy "$gstack_dir/lib" "$opencode_gstack/lib"
|
||||
fi
|
||||
if [ -d "$gstack_dir/browse/dist" ]; then
|
||||
_link_or_copy "$gstack_dir/browse/dist" "$opencode_gstack/browse/dist"
|
||||
fi
|
||||
|
|
@ -1115,6 +1125,7 @@ if [ "$INSTALL_KIRO" -eq 1 ]; then
|
|||
[ -L "$KIRO_GSTACK" ] && rm -f "$KIRO_GSTACK"
|
||||
mkdir -p "$KIRO_GSTACK" "$KIRO_GSTACK/browse" "$KIRO_GSTACK/gstack-upgrade" "$KIRO_GSTACK/review"
|
||||
_link_or_copy "$SOURCE_GSTACK_DIR/bin" "$KIRO_GSTACK/bin"
|
||||
_link_or_copy "$SOURCE_GSTACK_DIR/lib" "$KIRO_GSTACK/lib"
|
||||
_link_or_copy "$SOURCE_GSTACK_DIR/browse/dist" "$KIRO_GSTACK/browse/dist"
|
||||
_link_or_copy "$SOURCE_GSTACK_DIR/browse/bin" "$KIRO_GSTACK/browse/bin"
|
||||
# ETHOS.md — referenced by "Search Before Building" in all skill preambles
|
||||
|
|
|
|||
|
|
@ -2406,6 +2406,7 @@ describe('setup script validation', () => {
|
|||
expect(setupContent).toContain('kiro-cli');
|
||||
expect(setupContent).toContain('KIRO_SKILLS=');
|
||||
expect(setupContent).toContain('~/.kiro/skills/gstack');
|
||||
expect(setupContent).toContain('$KIRO_GSTACK/lib');
|
||||
});
|
||||
|
||||
test('setup supports --host opencode with install section and OpenCode skill path vars', () => {
|
||||
|
|
@ -2421,14 +2422,16 @@ describe('setup script validation', () => {
|
|||
expect(setupContent).toContain('qa/templates');
|
||||
expect(setupContent).toContain('qa/references');
|
||||
expect(setupContent).toContain('dx-hall-of-fame.md');
|
||||
expect(setupContent).toContain('$opencode_gstack/lib');
|
||||
});
|
||||
|
||||
test('create_agents_sidecar links runtime assets', () => {
|
||||
// Sidecar must link bin, browse, review, qa
|
||||
// Sidecar must link bin with its shared lib modules, plus browse, review, qa
|
||||
const fnStart = setupContent.indexOf('create_agents_sidecar()');
|
||||
const fnEnd = setupContent.indexOf('}', setupContent.indexOf('done', fnStart));
|
||||
const fnBody = setupContent.slice(fnStart, fnEnd);
|
||||
expect(fnBody).toContain('bin');
|
||||
expect(fnBody).toContain('lib');
|
||||
expect(fnBody).toContain('browse');
|
||||
expect(fnBody).toContain('review');
|
||||
expect(fnBody).toContain('qa');
|
||||
|
|
@ -2439,6 +2442,7 @@ describe('setup script validation', () => {
|
|||
const fnEnd = setupContent.indexOf('}', setupContent.indexOf('done', setupContent.indexOf('review/', fnStart)));
|
||||
const fnBody = setupContent.slice(fnStart, fnEnd);
|
||||
expect(fnBody).toContain('gstack/SKILL.md');
|
||||
expect(fnBody).toContain('$codex_gstack/lib');
|
||||
expect(fnBody).toContain('browse/dist');
|
||||
expect(fnBody).toContain('browse/bin');
|
||||
expect(fnBody).toContain('gstack-upgrade/SKILL.md');
|
||||
|
|
@ -2450,6 +2454,14 @@ describe('setup script validation', () => {
|
|||
expect(fnBody).not.toContain('_link_or_copy "$gstack_dir" "$codex_gstack"');
|
||||
});
|
||||
|
||||
test('create_factory_runtime_root links shared lib modules beside bin', () => {
|
||||
const fnStart = setupContent.indexOf('create_factory_runtime_root()');
|
||||
const fnEnd = setupContent.indexOf('create_opencode_runtime_root()', fnStart);
|
||||
const fnBody = setupContent.slice(fnStart, fnEnd);
|
||||
expect(fnBody).toContain('$factory_gstack/bin');
|
||||
expect(fnBody).toContain('$factory_gstack/lib');
|
||||
});
|
||||
|
||||
test('direct Codex installs are migrated out of ~/.codex/skills/gstack', () => {
|
||||
expect(setupContent).toContain('migrate_direct_codex_install');
|
||||
expect(setupContent).toContain('$HOME/.gstack/repos/gstack');
|
||||
|
|
|
|||
|
|
@ -0,0 +1,190 @@
|
|||
import { describe, test, expect } from 'bun:test';
|
||||
import { spawnSync } from 'child_process';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
|
||||
const ROOT = path.resolve(import.meta.dir, '..');
|
||||
const SETUP_SRC = fs.readFileSync(path.join(ROOT, 'setup'), 'utf-8');
|
||||
|
||||
// gstack-learnings-log is the command from the original bug report: bin scripts
|
||||
// import shared modules via `$SCRIPT_DIR/../lib`, so a runtime root that
|
||||
// exposes bin/ without lib/ fails with "Cannot find module .../lib/jsonl-store.ts".
|
||||
// Running it end-to-end from each installed root proves bin and lib travel together.
|
||||
const PAYLOAD = JSON.stringify({
|
||||
skill: 'review',
|
||||
type: 'pattern',
|
||||
key: 'runtime-lib-e2e',
|
||||
insight: 'bin commands resolve sibling lib modules after setup',
|
||||
confidence: 8,
|
||||
source: 'observed',
|
||||
});
|
||||
|
||||
// Slice a named shell function out of setup by its anchors so the tests are
|
||||
// resilient to line-number drift (same idiom as setup-windows-fallback.test.ts).
|
||||
function extractFunction(name: string): string {
|
||||
const start = SETUP_SRC.indexOf(`${name}() {`);
|
||||
const end = SETUP_SRC.indexOf('\n}\n', start);
|
||||
if (start < 0 || end < 0) throw new Error(`Could not locate ${name}() in setup`);
|
||||
return SETUP_SRC.slice(start, end + 2);
|
||||
}
|
||||
|
||||
// The Kiro install is an inline block, not a function. Slice from the runtime
|
||||
// root assignment through the last runtime-asset link so the extracted code is
|
||||
// a complete statement list.
|
||||
function extractKiroBlock(): string {
|
||||
const startAnchor = 'KIRO_GSTACK="$KIRO_SKILLS/gstack"';
|
||||
const endAnchor = '_link_or_copy "$SOURCE_GSTACK_DIR/browse/bin" "$KIRO_GSTACK/browse/bin"';
|
||||
const start = SETUP_SRC.indexOf(startAnchor);
|
||||
const end = SETUP_SRC.indexOf(endAnchor, start);
|
||||
if (start < 0 || end < 0) throw new Error('Could not locate the Kiro install block in setup');
|
||||
return SETUP_SRC.slice(start, end + endAnchor.length);
|
||||
}
|
||||
|
||||
interface CommandResult {
|
||||
buildStatus: number | null;
|
||||
buildStderr: string;
|
||||
runStatus: number | null;
|
||||
runStderr: string;
|
||||
learningsWritten: boolean;
|
||||
libIsSymlink: boolean | null;
|
||||
}
|
||||
|
||||
// Build one host runtime root inside a sandbox using the real setup shell code
|
||||
// (IS_WINDOWS toggles _link_or_copy between symlink and copy), then execute
|
||||
// gstack-learnings-log from the installed root and check the learning landed.
|
||||
function buildRootAndRunCommand(
|
||||
isWindows: '0' | '1',
|
||||
buildScript: (sandbox: string) => { script: string; rootDir: string },
|
||||
): CommandResult {
|
||||
const sandbox = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-runtime-lib-'));
|
||||
try {
|
||||
const home = path.join(sandbox, 'home');
|
||||
const project = path.join(sandbox, 'project');
|
||||
fs.mkdirSync(home, { recursive: true });
|
||||
fs.mkdirSync(project, { recursive: true });
|
||||
|
||||
const { script, rootDir } = buildScript(sandbox);
|
||||
const build = spawnSync(
|
||||
'bash',
|
||||
['-c', `IS_WINDOWS=${isWindows}\n${extractFunction('_link_or_copy')}\n${script}`],
|
||||
{ encoding: 'utf-8', timeout: 30000 },
|
||||
);
|
||||
|
||||
const libLst = fs.lstatSync(path.join(rootDir, 'lib'), { throwIfNoEntry: false });
|
||||
const run = spawnSync('bash', [path.join(rootDir, 'bin', 'gstack-learnings-log'), PAYLOAD], {
|
||||
cwd: project,
|
||||
encoding: 'utf-8',
|
||||
timeout: 30000,
|
||||
env: { ...process.env, HOME: home, GSTACK_HOME: path.join(home, '.gstack') },
|
||||
});
|
||||
|
||||
const projectsDir = path.join(home, '.gstack', 'projects');
|
||||
const learningsWritten = fs.existsSync(projectsDir)
|
||||
&& fs.readdirSync(projectsDir).some((slug) => {
|
||||
const file = path.join(projectsDir, slug, 'learnings.jsonl');
|
||||
return fs.existsSync(file) && fs.readFileSync(file, 'utf-8').includes('runtime-lib-e2e');
|
||||
});
|
||||
|
||||
return {
|
||||
buildStatus: build.status,
|
||||
buildStderr: build.stderr,
|
||||
runStatus: run.status,
|
||||
runStderr: run.stderr,
|
||||
learningsWritten,
|
||||
libIsSymlink: libLst ? libLst.isSymbolicLink() : null,
|
||||
};
|
||||
} finally {
|
||||
fs.rmSync(sandbox, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
// One builder per host root the lib fix touches. Each returns the shell that
|
||||
// setup itself runs plus where the installed runtime root lands.
|
||||
const HOST_ROOTS: Record<string, (sandbox: string) => { script: string; rootDir: string }> = {
|
||||
'agents sidecar': (sandbox) => ({
|
||||
script: [
|
||||
`SOURCE_GSTACK_DIR="${ROOT}"`,
|
||||
extractFunction('create_agents_sidecar'),
|
||||
`mkdir -p "${sandbox}/repo"`,
|
||||
`create_agents_sidecar "${sandbox}/repo"`,
|
||||
].join('\n'),
|
||||
rootDir: path.join(sandbox, 'repo', '.agents', 'skills', 'gstack'),
|
||||
}),
|
||||
codex: (sandbox) => ({
|
||||
script: [
|
||||
extractFunction('create_codex_runtime_root'),
|
||||
`create_codex_runtime_root "${ROOT}" "${sandbox}/home/.codex/skills/gstack"`,
|
||||
].join('\n'),
|
||||
rootDir: path.join(sandbox, 'home', '.codex', 'skills', 'gstack'),
|
||||
}),
|
||||
factory: (sandbox) => ({
|
||||
script: [
|
||||
extractFunction('create_factory_runtime_root'),
|
||||
`create_factory_runtime_root "${ROOT}" "${sandbox}/home/.factory/skills/gstack"`,
|
||||
].join('\n'),
|
||||
rootDir: path.join(sandbox, 'home', '.factory', 'skills', 'gstack'),
|
||||
}),
|
||||
opencode: (sandbox) => ({
|
||||
script: [
|
||||
extractFunction('create_opencode_runtime_root'),
|
||||
`create_opencode_runtime_root "${ROOT}" "${sandbox}/home/.opencode/skills/gstack"`,
|
||||
].join('\n'),
|
||||
rootDir: path.join(sandbox, 'home', '.opencode', 'skills', 'gstack'),
|
||||
}),
|
||||
kiro: (sandbox) => ({
|
||||
script: [
|
||||
`HOME="${sandbox}/home"`,
|
||||
`SOURCE_GSTACK_DIR="${ROOT}"`,
|
||||
`KIRO_SKILLS="$HOME/.kiro/skills"`,
|
||||
`mkdir -p "$KIRO_SKILLS"`,
|
||||
extractKiroBlock(),
|
||||
].join('\n'),
|
||||
rootDir: path.join(sandbox, 'home', '.kiro', 'skills', 'gstack'),
|
||||
}),
|
||||
};
|
||||
|
||||
// The IS_WINDOWS=0 cells rely on Unix `ln -snf` semantics; on a real Windows
|
||||
// runner without Developer Mode that silently degrades to a copy — the exact
|
||||
// bug _link_or_copy works around — so skip there, matching the behavior-matrix
|
||||
// precedent in setup-windows-fallback.test.ts. The IS_WINDOWS=1 cells exercise
|
||||
// the Windows copy branch itself, which is plain `cp -R` and portable.
|
||||
describe.skipIf(process.platform === 'win32')('setup: bin commands resolve sibling lib from every host root', () => {
|
||||
for (const [host, buildScript] of Object.entries(HOST_ROOTS)) {
|
||||
test(`${host} root (symlink install): gstack-learnings-log imports ../lib and writes the learning`, () => {
|
||||
const r = buildRootAndRunCommand('0', buildScript);
|
||||
expect(r.buildStatus).toBe(0);
|
||||
expect(r.libIsSymlink).toBe(true);
|
||||
expect(r.runStderr).not.toContain('lib/jsonl-store.ts');
|
||||
expect(r.runStatus).toBe(0);
|
||||
expect(r.learningsWritten).toBe(true);
|
||||
});
|
||||
|
||||
test(`${host} root (Windows copy install): gstack-learnings-log imports ../lib and writes the learning`, () => {
|
||||
const r = buildRootAndRunCommand('1', buildScript);
|
||||
expect(r.buildStatus).toBe(0);
|
||||
// Windows branch copies: lib must be a real directory, not a symlink.
|
||||
expect(r.libIsSymlink).toBe(false);
|
||||
expect(r.runStderr).not.toContain('lib/jsonl-store.ts');
|
||||
expect(r.runStatus).toBe(0);
|
||||
expect(r.learningsWritten).toBe(true);
|
||||
});
|
||||
}
|
||||
|
||||
// Negative control: a root with bin/ but no lib/ (the pre-fix layout) must
|
||||
// fail on the ../lib import. Proves the positive cells actually detect the
|
||||
// regression rather than passing vacuously.
|
||||
test('a root missing lib/ beside bin/ fails the ../lib import (pre-fix layout)', () => {
|
||||
const r = buildRootAndRunCommand('0', (sandbox) => ({
|
||||
script: [
|
||||
`mkdir -p "${sandbox}/broken"`,
|
||||
`_link_or_copy "${ROOT}/bin" "${sandbox}/broken/bin"`,
|
||||
].join('\n'),
|
||||
rootDir: path.join(sandbox, 'broken'),
|
||||
}));
|
||||
expect(r.buildStatus).toBe(0);
|
||||
expect(r.runStatus).not.toBe(0);
|
||||
expect(r.runStderr).toContain('lib/jsonl-store.ts');
|
||||
expect(r.learningsWritten).toBe(false);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue