mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-21 20:28:24 +08:00
fix: resolve merge conflicts with main — keep stealth patches + auth token
This commit is contained in:
@@ -135,4 +135,62 @@ describe('gstack-config', () => {
|
||||
const { stdout } = run(['get', 'test_special']);
|
||||
expect(stdout).toBe('a/b&c\\d');
|
||||
});
|
||||
|
||||
// ─── annotated header ──────────────────────────────────────
|
||||
test('first set writes annotated header with docs', () => {
|
||||
run(['set', 'telemetry', 'off']);
|
||||
const content = readFileSync(join(stateDir, 'config.yaml'), 'utf-8');
|
||||
expect(content).toContain('# gstack configuration');
|
||||
expect(content).toContain('edit freely');
|
||||
expect(content).toContain('proactive:');
|
||||
expect(content).toContain('telemetry:');
|
||||
expect(content).toContain('auto_upgrade:');
|
||||
expect(content).toContain('skill_prefix:');
|
||||
expect(content).toContain('routing_declined:');
|
||||
expect(content).toContain('codex_reviews:');
|
||||
expect(content).toContain('skip_eng_review:');
|
||||
});
|
||||
|
||||
test('header written only once, not duplicated on second set', () => {
|
||||
run(['set', 'foo', 'bar']);
|
||||
run(['set', 'baz', 'qux']);
|
||||
const content = readFileSync(join(stateDir, 'config.yaml'), 'utf-8');
|
||||
const headerCount = (content.match(/# gstack configuration/g) || []).length;
|
||||
expect(headerCount).toBe(1);
|
||||
});
|
||||
|
||||
test('header does not break get on commented-out keys', () => {
|
||||
run(['set', 'telemetry', 'community']);
|
||||
// Header contains "# telemetry: anonymous" as a comment example.
|
||||
// get should return the real value, not the comment.
|
||||
const { stdout } = run(['get', 'telemetry']);
|
||||
expect(stdout).toBe('community');
|
||||
});
|
||||
|
||||
test('existing config file is not overwritten with header', () => {
|
||||
writeFileSync(join(stateDir, 'config.yaml'), 'existing: value\n');
|
||||
run(['set', 'new_key', 'new_value']);
|
||||
const content = readFileSync(join(stateDir, 'config.yaml'), 'utf-8');
|
||||
expect(content).toContain('existing: value');
|
||||
expect(content).not.toContain('# gstack configuration');
|
||||
});
|
||||
|
||||
// ─── routing_declined ──────────────────────────────────────
|
||||
test('routing_declined defaults to empty (not set)', () => {
|
||||
const { stdout } = run(['get', 'routing_declined']);
|
||||
expect(stdout).toBe('');
|
||||
});
|
||||
|
||||
test('routing_declined can be set and read', () => {
|
||||
run(['set', 'routing_declined', 'true']);
|
||||
const { stdout } = run(['get', 'routing_declined']);
|
||||
expect(stdout).toBe('true');
|
||||
});
|
||||
|
||||
test('routing_declined can be reset to false', () => {
|
||||
run(['set', 'routing_declined', 'true']);
|
||||
run(['set', 'routing_declined', 'false']);
|
||||
const { stdout } = run(['get', 'routing_declined']);
|
||||
expect(stdout).toBe('false');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -513,17 +513,17 @@ describe('BROWSE_TAB tab pinning (cross-tab isolation)', () => {
|
||||
expect(handleFn).toContain('tabId');
|
||||
// Should save and restore the active tab
|
||||
expect(handleFn).toContain('savedTabId');
|
||||
expect(handleFn).toContain('browserManager.switchTab(tabId)');
|
||||
expect(handleFn).toContain('switchTab(tabId');
|
||||
});
|
||||
|
||||
test('handleCommand restores active tab after command (success path)', () => {
|
||||
// On success, should restore savedTabId
|
||||
// On success, should restore savedTabId without stealing focus
|
||||
const handleFn = serverSrc.slice(
|
||||
serverSrc.indexOf('async function handleCommand('),
|
||||
serverSrc.length,
|
||||
);
|
||||
// Count restore calls — should appear in both success and error paths
|
||||
const restoreCount = (handleFn.match(/browserManager\.switchTab\(savedTabId\)/g) || []).length;
|
||||
const restoreCount = (handleFn.match(/switchTab\(savedTabId/g) || []).length;
|
||||
expect(restoreCount).toBeGreaterThanOrEqual(2); // success + error paths
|
||||
});
|
||||
|
||||
@@ -532,7 +532,7 @@ describe('BROWSE_TAB tab pinning (cross-tab isolation)', () => {
|
||||
const catchBlock = serverSrc.slice(
|
||||
serverSrc.indexOf('} catch (err: any) {', serverSrc.indexOf('async function handleCommand(')),
|
||||
);
|
||||
expect(catchBlock).toContain('switchTab(savedTabId)');
|
||||
expect(catchBlock).toContain('switchTab(savedTabId');
|
||||
});
|
||||
|
||||
test('tab pinning only activates when tabId is provided', () => {
|
||||
|
||||
@@ -41,13 +41,13 @@ describe('sidebar system prompt (server.ts)', () => {
|
||||
expect(promptSection).toContain('url`');
|
||||
});
|
||||
|
||||
test('system prompt includes narration instructions', () => {
|
||||
test('system prompt includes conciseness and stop instructions', () => {
|
||||
const promptSection = serverSrc.slice(
|
||||
serverSrc.indexOf('const systemPrompt = ['),
|
||||
serverSrc.indexOf("].join('\\n');", serverSrc.indexOf('const systemPrompt = [')) + 15,
|
||||
);
|
||||
expect(promptSection).toContain('Narrate');
|
||||
expect(promptSection).toContain('plain English');
|
||||
expect(promptSection).toContain('CONCISE');
|
||||
expect(promptSection).toContain('STOP');
|
||||
});
|
||||
|
||||
test('--resume is never used in spawnClaude args', () => {
|
||||
@@ -385,12 +385,11 @@ describe('browser tab bar (sidepanel.html)', () => {
|
||||
describe('sidebar→browser tab switch', () => {
|
||||
const bmSrc = fs.readFileSync(path.join(ROOT, 'src', 'browser-manager.ts'), 'utf-8');
|
||||
|
||||
test('switchTab calls bringToFront so browser visually switches', () => {
|
||||
const switchFn = bmSrc.slice(
|
||||
bmSrc.indexOf('switchTab(id: number)'),
|
||||
bmSrc.indexOf('switchTab(id: number)') + 400,
|
||||
);
|
||||
expect(switchFn).toContain('bringToFront');
|
||||
test('switchTab supports bringToFront option', () => {
|
||||
expect(bmSrc).toContain('switchTab(id: number, opts?');
|
||||
expect(bmSrc).toContain('bringToFront');
|
||||
// Default behavior still brings to front (opt-out, not opt-in)
|
||||
expect(bmSrc).toContain('bringToFront !== false');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -940,6 +939,82 @@ describe('chat toolbar buttons disabled state', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Chat message dedup ─────────────────────────────────────────
|
||||
|
||||
describe('chat message dedup (prevents repeat rendering)', () => {
|
||||
const js = fs.readFileSync(path.join(ROOT, '..', 'extension', 'sidepanel.js'), 'utf-8');
|
||||
|
||||
test('renderedEntryIds Set exists for dedup tracking', () => {
|
||||
expect(js).toContain('const renderedEntryIds = new Set()');
|
||||
});
|
||||
|
||||
test('addChatEntry checks entry.id against renderedEntryIds', () => {
|
||||
const addFn = js.slice(
|
||||
js.indexOf('function addChatEntry(entry)'),
|
||||
js.indexOf('\n // User messages', js.indexOf('function addChatEntry(entry)')),
|
||||
);
|
||||
expect(addFn).toContain('renderedEntryIds.has(entry.id)');
|
||||
expect(addFn).toContain('renderedEntryIds.add(entry.id)');
|
||||
// Should return early (skip) if already rendered
|
||||
expect(addFn).toContain('return');
|
||||
});
|
||||
|
||||
test('addChatEntry skips dedup for entries without id (local notifications)', () => {
|
||||
const addFn = js.slice(
|
||||
js.indexOf('function addChatEntry(entry)'),
|
||||
js.indexOf('\n // User messages', js.indexOf('function addChatEntry(entry)')),
|
||||
);
|
||||
// Should only check dedup when entry.id is defined
|
||||
expect(addFn).toContain('entry.id !== undefined');
|
||||
});
|
||||
|
||||
test('clear chat resets renderedEntryIds', () => {
|
||||
expect(js).toContain('renderedEntryIds.clear()');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Agent conciseness and focus stealing ───────────────────────
|
||||
|
||||
describe('sidebar agent conciseness + no focus stealing', () => {
|
||||
const serverSrc = fs.readFileSync(path.join(ROOT, 'src', 'server.ts'), 'utf-8');
|
||||
const bmSrc = fs.readFileSync(path.join(ROOT, 'src', 'browser-manager.ts'), 'utf-8');
|
||||
|
||||
test('system prompt tells agent to STOP when task is done', () => {
|
||||
const promptSection = serverSrc.slice(
|
||||
serverSrc.indexOf('const systemPrompt = ['),
|
||||
serverSrc.indexOf("].join('\\n');", serverSrc.indexOf('const systemPrompt = [')),
|
||||
);
|
||||
expect(promptSection).toContain('STOP');
|
||||
expect(promptSection).toContain('CONCISE');
|
||||
expect(promptSection).toContain('Do NOT keep exploring');
|
||||
});
|
||||
|
||||
test('sidebar agent uses opus (not sonnet) for prompt injection resistance', () => {
|
||||
const spawnFn = serverSrc.slice(
|
||||
serverSrc.indexOf('function spawnClaude('),
|
||||
serverSrc.indexOf('\nfunction ', serverSrc.indexOf('function spawnClaude(') + 1),
|
||||
);
|
||||
expect(spawnFn).toContain("'opus'");
|
||||
});
|
||||
|
||||
test('switchTab has bringToFront option', () => {
|
||||
expect(bmSrc).toContain('bringToFront?: boolean');
|
||||
expect(bmSrc).toContain('bringToFront !== false');
|
||||
});
|
||||
|
||||
test('handleCommand tab pinning does NOT steal focus', () => {
|
||||
// All switchTab calls in handleCommand should use bringToFront: false
|
||||
const handleFn = serverSrc.slice(
|
||||
serverSrc.indexOf('async function handleCommand('),
|
||||
serverSrc.indexOf('\n// ', serverSrc.indexOf('async function handleCommand(') + 200),
|
||||
);
|
||||
const switchCalls = handleFn.match(/switchTab\([^)]+\)/g) || [];
|
||||
for (const call of switchCalls) {
|
||||
expect(call).toContain('bringToFront: false');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ─── LLM-based cleanup architecture ─────────────────────────────
|
||||
|
||||
describe('LLM-based cleanup (smart agent cleanup)', () => {
|
||||
|
||||
Reference in New Issue
Block a user