mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-02 15:04:37 +01:00
B5: learnings and timeline reads fail loudly when bun is missing
gstack-learnings-search and gstack-timeline-read ended in '2>/dev/null || exit 0', so a missing bun or a crashed embedded script printed nothing and exited 0, identical to 'nothing recorded'. Both now exit 127 with '<script>: bun not found on PATH, so ... could not be read. Fix: install Bun (https://bun.sh), then re-run ./setup.' and let an embedded-script failure exit non-zero with its stderr. A successful run with no matches still exits 0 with empty output. The generated callers that print 'LEARNINGS: unavailable (...)' belong to the generator lane. Co-authored-by: loulanyue <260355617@qq.com>
This commit is contained in:
1 parent
373118784a
commit
85c9d4cebd
3 files changed
+69
-6
No files matched your search
@@ -4,7 +4,9 @@
|
||||
#
|
||||
# Reads ~/.gstack/projects/$SLUG/learnings.jsonl, applies confidence decay,
|
||||
# resolves duplicates (latest winner per key+type), and outputs formatted text.
|
||||
# Exit 0 silently if no learnings file exists.
|
||||
# Exit 0 silently if no learnings file exists or nothing matches. Missing bun
|
||||
# exits 127 and a failed read exits non-zero with its stderr (#2790): "could
|
||||
# not read" must never look like "nothing recorded".
|
||||
set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
eval "$("$SCRIPT_DIR/gstack-slug" 2>/dev/null)"
|
||||
@@ -45,6 +47,11 @@ if [ ! -f "$LEARNINGS_FILE" ] && [ ${#CROSS_FILES[@]} -eq 0 ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
command -v bun >/dev/null 2>&1 || {
|
||||
echo "gstack-learnings-search: bun not found on PATH, so learnings could not be read. Fix: install Bun (https://bun.sh), then re-run ./setup." >&2
|
||||
exit 127
|
||||
}
|
||||
|
||||
emit_tagged_file() {
|
||||
local tag="$1"
|
||||
local file="$2"
|
||||
@@ -161,4 +168,4 @@ for (const [t, arr] of Object.entries(byType)) {
|
||||
}
|
||||
console.log('');
|
||||
}
|
||||
" 2>/dev/null || exit 0
|
||||
"
|
||||
@@ -4,7 +4,8 @@
|
||||
#
|
||||
# Session timeline: local-only, never sent anywhere.
|
||||
# Reads ~/.gstack/projects/$SLUG/timeline.jsonl, filters, formats.
|
||||
# Exit 0 silently if no timeline file exists.
|
||||
# Exit 0 silently if no timeline file exists or nothing matches. Missing bun
|
||||
# exits 127 and a failed read exits non-zero with its stderr (#2790).
|
||||
set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
eval "$("$SCRIPT_DIR/gstack-slug" 2>/dev/null)"
|
||||
@@ -31,7 +32,12 @@ if [ ! -f "$TIMELINE_FILE" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
cat "$TIMELINE_FILE" 2>/dev/null | GSTACK_TIMELINE_SINCE="$SINCE" GSTACK_TIMELINE_BRANCH="$BRANCH" GSTACK_TIMELINE_LIMIT="$LIMIT" bun -e "
|
||||
command -v bun >/dev/null 2>&1 || {
|
||||
echo "gstack-timeline-read: bun not found on PATH, so the timeline could not be read. Fix: install Bun (https://bun.sh), then re-run ./setup." >&2
|
||||
exit 127
|
||||
}
|
||||
|
||||
cat "$TIMELINE_FILE" | GSTACK_TIMELINE_SINCE="$SINCE" GSTACK_TIMELINE_BRANCH="$BRANCH" GSTACK_TIMELINE_LIMIT="$LIMIT" bun -e "
|
||||
const lines = (await Bun.stdin.text()).trim().split('\n').filter(Boolean);
|
||||
const since = process.env.GSTACK_TIMELINE_SINCE || '';
|
||||
const branch = process.env.GSTACK_TIMELINE_BRANCH || '';
|
||||
@@ -95,4 +101,4 @@ for (const e of recent) {
|
||||
const outcome = e.outcome ? ' [' + e.outcome + ']' : '';
|
||||
console.log('- ' + ts + ' /' + e.skill + ' ' + e.event + outcome + dur + (e.branch ? ' on ' + e.branch : ''));
|
||||
}
|
||||
" 2>/dev/null || exit 0
|
||||
"
|
||||
@@ -2,7 +2,7 @@ import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import { execFileSync } from 'child_process';
|
||||
import { execFileSync, spawnSync } from 'child_process';
|
||||
|
||||
const ROOT = path.resolve(import.meta.dir, '..');
|
||||
const BIN = path.join(ROOT, 'bin', 'gstack-learnings-search');
|
||||
@@ -91,3 +91,53 @@ describe('gstack-learnings-search cross-project trust gating', () => {
|
||||
expect(out).not.toContain('foreign-legacy');
|
||||
});
|
||||
});
|
||||
|
||||
// B5 (#2790): both query scripts ended in `2>/dev/null || exit 0`, so a
|
||||
// missing bun or a crashed embedded script printed nothing and exited 0,
|
||||
// exactly like "nothing recorded".
|
||||
describe('B5: query scripts never report a failed read as an empty result', () => {
|
||||
const TOOLS = ['bash', 'sh', 'env', 'dirname', 'basename', 'git', 'tr', 'sed', 'cat', 'head', 'tail', 'grep', 'find',
|
||||
'ls', 'mkdir', 'mktemp', 'mv', 'rm', 'cp', 'awk', 'wc', 'sort', 'uname', 'date', 'cut', 'readlink', 'realpath', 'stat', 'touch', 'id', 'printf'];
|
||||
const shimDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-nobun-'));
|
||||
const failDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-failbun-'));
|
||||
for (const tool of TOOLS) {
|
||||
const r = spawnSync('bash', ['-c', `command -v ${tool}`], { encoding: 'utf-8', timeout: 5000 });
|
||||
const where = (r.stdout || '').trim();
|
||||
if (where.startsWith('/')) fs.symlinkSync(where, path.join(shimDir, tool));
|
||||
}
|
||||
fs.writeFileSync(path.join(failDir, 'bun'), '#!/bin/sh\necho "embedded script exploded" >&2\nexit 3\n', { mode: 0o755 });
|
||||
const timelineDir = path.join(tmpHome, 'projects', slug);
|
||||
afterAll(() => {
|
||||
fs.rmSync(shimDir, { recursive: true, force: true });
|
||||
fs.rmSync(failDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
function runWith(script: string, pathValue: string, args: string[] = []) {
|
||||
return spawnSync('bash', [path.join(ROOT, 'bin', script), ...args], {
|
||||
timeout: 30_000, env: { ...process.env, GSTACK_HOME: tmpHome, PATH: pathValue }, cwd: tmpCwd, encoding: 'utf-8',
|
||||
});
|
||||
}
|
||||
|
||||
for (const script of ['gstack-learnings-search', 'gstack-timeline-read']) {
|
||||
test(`${script}: bun missing exits 127 with the fix line`, () => {
|
||||
fs.writeFileSync(path.join(timelineDir, 'timeline.jsonl'), JSON.stringify({ ts: '2026-05-01T00:00:00Z', skill: 'ship', event: 'started', branch: 'main' }) + '\n');
|
||||
const r = runWith(script, shimDir);
|
||||
expect(r.status).toBe(127);
|
||||
expect(r.stdout).toBe('');
|
||||
expect(r.stderr).toContain(`${script}: bun not found on PATH`);
|
||||
expect(r.stderr).toContain('Fix: install Bun (https://bun.sh), then re-run ./setup.');
|
||||
});
|
||||
|
||||
test(`${script}: a failing embedded script exits non-zero and keeps its stderr`, () => {
|
||||
const r = runWith(script, `${failDir}:${process.env.PATH}`);
|
||||
expect(r.status).not.toBe(0);
|
||||
expect(r.stderr).toContain('embedded script exploded');
|
||||
});
|
||||
}
|
||||
|
||||
test('a successful search with no matches still exits 0 with empty output', () => {
|
||||
const r = runWith('gstack-learnings-search', process.env.PATH!, ['--query', 'zzz-no-such-token']);
|
||||
expect(r.status).toBe(0);
|
||||
expect(r.stdout).toBe('');
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user