fix: resolve merge conflicts with main — keep stealth patches + auth token

This commit is contained in:
Garry Tan
2026-03-30 20:47:51 -07:00
65 changed files with 5234 additions and 574 deletions

View File

@@ -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');
});
});

View File

@@ -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', () => {

View File

@@ -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)', () => {