feat(tracker): support extension-contributed tracker directory and ensure project precedence

- Add 'tracker' property to ExtensionConfig to allow extensions to specify task tracking directories.
- Fix extension loading to pass tracker configuration from extensions.
- Implement project-level precedence for tracker directory over extension defaults.
- Add comprehensive tests for tracker directory resolution and precedence.
This commit is contained in:
Moisés Gana Obregón
2026-03-25 23:01:12 +00:00
parent 60be21f92e
commit cb507166bd
9 changed files with 223 additions and 5 deletions
+61
View File
@@ -3688,6 +3688,46 @@ describe('loadCliConfig mcpEnabled', () => {
expect(config.storage.getPlansDir()).not.toContain('ext-plans-dir');
});
it('should use tracker directory from active extension when user has not specified one', async () => {
process.argv = ['node', 'script.js'];
const settings = createTestMergedSettings({
experimental: { plan: true },
});
const argv = await parseArguments(settings);
vi.spyOn(ExtensionManager.prototype, 'getExtensions').mockReturnValue([
{
name: 'ext-tracker',
isActive: true,
tracker: { directory: 'ext-tracker-dir' },
} as unknown as GeminiCLIExtension,
]);
const config = await loadCliConfig(settings, 'test-session', argv);
expect(config.storage.getTrackerDir()).toContain('ext-tracker-dir');
});
it('should NOT use tracker directory from active extension when user has specified one', async () => {
process.argv = ['node', 'script.js'];
const settings = createTestMergedSettings({
general: { tracker: { directory: 'user-tracker-dir' } },
experimental: { plan: true },
});
const argv = await parseArguments(settings);
vi.spyOn(ExtensionManager.prototype, 'getExtensions').mockReturnValue([
{
name: 'ext-tracker',
isActive: true,
tracker: { directory: 'ext-tracker-dir' },
} as unknown as GeminiCLIExtension,
]);
const config = await loadCliConfig(settings, 'test-session', argv);
expect(config.storage.getTrackerDir()).toContain('user-tracker-dir');
expect(config.storage.getTrackerDir()).not.toContain('ext-tracker-dir');
});
it('should NOT use plan directory from inactive extension', async () => {
process.argv = ['node', 'script.js'];
const settings = createTestMergedSettings({
@@ -3709,6 +3749,27 @@ describe('loadCliConfig mcpEnabled', () => {
);
});
it('should NOT use tracker directory from inactive extension', async () => {
process.argv = ['node', 'script.js'];
const settings = createTestMergedSettings({
experimental: { plan: true },
});
const argv = await parseArguments(settings);
vi.spyOn(ExtensionManager.prototype, 'getExtensions').mockReturnValue([
{
name: 'ext-tracker',
isActive: false,
tracker: { directory: 'ext-tracker-dir-inactive' },
} as unknown as GeminiCLIExtension,
]);
const config = await loadCliConfig(settings, 'test-session', argv);
expect(config.storage.getTrackerDir()).not.toContain(
'ext-tracker-dir-inactive',
);
});
it('should use default path if neither user nor extension settings provide a plan directory', async () => {
process.argv = ['node', 'script.js'];
const settings = createTestMergedSettings({
+13 -2
View File
@@ -570,9 +570,17 @@ export async function loadCliConfig(
});
await extensionManager.loadExtensions();
const extensionPlanSettings = extensionManager
const activeExtensions = extensionManager
.getExtensions()
.find((ext) => ext.isActive && ext.plan?.directory)?.plan;
.filter((ext) => ext.isActive);
const extensionPlanSettings = activeExtensions.find(
(ext) => !!ext.plan?.directory,
)?.plan;
const extensionTrackerSettings = activeExtensions.find(
(ext) => !!ext.tracker?.directory,
)?.tracker;
const experimentalJitContext = settings.experimental.jitContext;
@@ -931,6 +939,9 @@ export async function loadCliConfig(
planSettings: settings.general?.plan?.directory
? settings.general.plan
: (extensionPlanSettings ?? settings.general?.plan),
trackerSettings: settings.general?.tracker?.directory
? settings.general.tracker
: (extensionTrackerSettings ?? settings.general?.tracker),
enableEventDrivenScheduler: true,
skillsSupport: settings.skills?.enabled ?? true,
disabledSkills: settings.skills?.disabled,
@@ -1050,6 +1050,7 @@ Would you like to attempt to install via "git clone" instead?`,
rules,
checkers,
plan: config.plan,
tracker: config.tracker,
};
} catch (e) {
const extName = path.basename(extensionDir);
+9
View File
@@ -42,6 +42,15 @@ export interface ExtensionConfig {
*/
directory?: string;
};
/**
* Task tracking configuration contributed by this extension.
*/
tracker?: {
/**
* The directory where task tracking data is stored.
*/
directory?: string;
};
/**
* Used to migrate an extension to a new repository source.
*/
@@ -125,6 +125,16 @@ describe('SettingsSchema', () => {
).toBe('string');
});
it('should have tracker nested properties', () => {
expect(
getSettingsSchema().general?.properties?.tracker?.properties?.directory,
).toBeDefined();
expect(
getSettingsSchema().general?.properties?.tracker?.properties?.directory
.type,
).toBe('string');
});
it('should have fileFiltering nested properties', () => {
expect(
getSettingsSchema().context.properties.fileFiltering.properties
+21
View File
@@ -315,6 +315,27 @@ const SETTINGS_SCHEMA = {
},
},
},
tracker: {
type: 'object',
label: 'Task Tracker',
category: 'General',
requiresRestart: true,
default: {},
description: 'Task tracking features configuration.',
showInDialog: false,
properties: {
directory: {
type: 'string',
label: 'Tracker Directory',
category: 'General',
requiresRestart: true,
default: undefined as string | undefined,
description:
'The directory where task tracking data is stored. If not specified, defaults to the system temporary directory.',
showInDialog: true,
},
},
},
retryFetchErrors: {
type: 'boolean',
label: 'Retry Fetch Errors',