Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
272 changes: 272 additions & 0 deletions src/__tests__/wiki-toggle-team.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,272 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { resolveWikiEnabled, LocalConfigSchema, TeamaiConfigSchema } from '../types.js';
import type { TeamaiConfig, LocalConfig } from '../types.js';

describe('resolveWikiEnabled', () => {
const baseLocalConfig: LocalConfig = {
repo: { localPath: '/tmp/repo', remote: 'https://git.woa.com/test/repo.git' },
username: 'testuser',
scope: 'user',
additionalRoles: [],
};

const baseTeamConfig: TeamaiConfig = {
team: 'test-team',
description: '',
repo: 'https://git.woa.com/test/repo.git',
provider: 'tgit',
reviewers: [],
sharing: {
skills: {},
rules: { enforced: [] },
docs: { localDir: '~/.teamai/docs' },
env: { injectShellProfile: true },
},
toolPaths: {},
};

it('returns true when team has no wiki section and no local override', () => {
expect(resolveWikiEnabled(baseTeamConfig, baseLocalConfig)).toBe(true);
});

it('returns true when team wiki.enabled is true and no local override', () => {
const teamConfig = {
...baseTeamConfig,
sharing: { ...baseTeamConfig.sharing, wiki: { enabled: true } },
};
expect(resolveWikiEnabled(teamConfig, baseLocalConfig)).toBe(true);
});

it('returns false when team wiki.enabled is false and no local override', () => {
const teamConfig = {
...baseTeamConfig,
sharing: { ...baseTeamConfig.sharing, wiki: { enabled: false } },
};
expect(resolveWikiEnabled(teamConfig, baseLocalConfig)).toBe(false);
});

it('returns false when team enabled + local override false (local wins)', () => {
const teamConfig = {
...baseTeamConfig,
sharing: { ...baseTeamConfig.sharing, wiki: { enabled: true } },
};
const localConfig = { ...baseLocalConfig, wikiEnabled: false };
expect(resolveWikiEnabled(teamConfig, localConfig)).toBe(false);
});

it('returns true when team disabled + local override true (local wins)', () => {
const teamConfig = {
...baseTeamConfig,
sharing: { ...baseTeamConfig.sharing, wiki: { enabled: false } },
};
const localConfig = { ...baseLocalConfig, wikiEnabled: true };
expect(resolveWikiEnabled(teamConfig, localConfig)).toBe(true);
});

it('backward compat: team config without wiki section defaults to enabled', () => {
expect(resolveWikiEnabled(baseTeamConfig, baseLocalConfig)).toBe(true);
});

it('backward compat: old local config without wikiEnabled follows team config', () => {
const teamConfig = {
...baseTeamConfig,
sharing: { ...baseTeamConfig.sharing, wiki: { enabled: false } },
};
expect(resolveWikiEnabled(teamConfig, baseLocalConfig)).toBe(false);
});
});

describe('LocalConfigSchema wikiEnabled', () => {
const baseInput = {
repo: { localPath: '/tmp/repo', remote: 'https://git.woa.com/test/repo.git' },
username: 'test-user',
};

it('defaults wikiEnabled to undefined when not provided', () => {
const config = LocalConfigSchema.parse(baseInput);
expect(config.wikiEnabled).toBeUndefined();
});

it('accepts wikiEnabled: true', () => {
const config = LocalConfigSchema.parse({ ...baseInput, wikiEnabled: true });
expect(config.wikiEnabled).toBe(true);
});

it('accepts wikiEnabled: false', () => {
const config = LocalConfigSchema.parse({ ...baseInput, wikiEnabled: false });
expect(config.wikiEnabled).toBe(false);
});

it('rejects non-boolean wikiEnabled', () => {
expect(() => LocalConfigSchema.parse({ ...baseInput, wikiEnabled: 'no' })).toThrow();
});

it('backward compat: old configs without wikiEnabled parse correctly', () => {
const oldConfig = {
repo: { localPath: '/home/user/.teamai/team-repo', remote: 'git@git.woa.com:team/repo.git' },
username: 'alice',
scope: 'user',
primaryRole: 'backend',
additionalRoles: ['infra'],
subscribedTags: ['api', 'core'],
};
const config = LocalConfigSchema.parse(oldConfig);
expect(config.wikiEnabled).toBeUndefined();
expect(config.primaryRole).toBe('backend');
});
});

describe('TeamaiConfigSchema wiki in sharing', () => {
const baseInput = {
team: 'my-team',
repo: 'https://git.woa.com/test/repo.git',
};

it('defaults wiki to undefined when not in sharing config', () => {
const config = TeamaiConfigSchema.parse(baseInput);
expect(config.sharing.wiki).toBeUndefined();
});

it('accepts wiki.enabled: true', () => {
const config = TeamaiConfigSchema.parse({
...baseInput,
sharing: { wiki: { enabled: true } },
});
expect(config.sharing.wiki).toEqual({ enabled: true });
});

it('accepts wiki.enabled: false with disabledHint', () => {
const config = TeamaiConfigSchema.parse({
...baseInput,
sharing: { wiki: { enabled: false, disabledHint: 'use external team-wiki plugin' } },
});
expect(config.sharing.wiki).toEqual({ enabled: false, disabledHint: 'use external team-wiki plugin' });
});
});

describe('pull skips wiki when resolved to disabled', () => {
it('filters wiki from resource types when disabled', () => {
const wikiEnabled = false;
const resourceTypes = wikiEnabled
? ['skills', 'rules', 'docs', 'env', 'wiki']
: ['skills', 'rules', 'docs', 'env'];
expect(resourceTypes).not.toContain('wiki');
});

it('includes wiki in resource types when enabled', () => {
const wikiEnabled = true;
const resourceTypes = wikiEnabled
? ['skills', 'rules', 'docs', 'env', 'wiki']
: ['skills', 'rules', 'docs', 'env'];
expect(resourceTypes).toContain('wiki');
});
});

describe('builtin-skills skips teamai-wiki when disabled', () => {
it('filters teamai-wiki from skill names when skipWiki is true', () => {
const skillNames = ['teamai-share-learnings', 'teamai-wiki'];
const filteredSkills = true
? skillNames.filter(name => name !== 'teamai-wiki')
: skillNames;
expect(filteredSkills).toEqual(['teamai-share-learnings']);
});

it('keeps teamai-wiki when skipWiki is false/undefined', () => {
const skillNames = ['teamai-share-learnings', 'teamai-wiki'];
const filteredSkills = false
? skillNames.filter(name => name !== 'teamai-wiki')
: skillNames;
expect(filteredSkills).toEqual(['teamai-share-learnings', 'teamai-wiki']);
});
});

describe('resolveWikiEnabled - environment variable override', () => {
const baseLocalConfig = {
repo: { localPath: '/tmp/repo', remote: 'https://git.woa.com/test/repo.git' },
username: 'testuser',
scope: 'user' as const,
additionalRoles: [] as string[],
};

const baseTeamConfig = {
team: 'test-team',
description: '',
repo: 'https://git.woa.com/test/repo.git',
provider: 'tgit' as const,
reviewers: [] as string[],
sharing: {
skills: {},
rules: { enforced: [] as string[] },
docs: { localDir: '~/.teamai/docs' },
env: { injectShellProfile: true },
},
toolPaths: {},
};

let originalWikiDisabled: string | undefined;
let originalWikiEnabled: string | undefined;

beforeEach(() => {
originalWikiDisabled = process.env.TEAMAI_WIKI_DISABLED;
originalWikiEnabled = process.env.TEAMAI_WIKI_ENABLED;
delete process.env.TEAMAI_WIKI_DISABLED;
delete process.env.TEAMAI_WIKI_ENABLED;
});

afterEach(() => {
if (originalWikiDisabled !== undefined) {
process.env.TEAMAI_WIKI_DISABLED = originalWikiDisabled;
} else {
delete process.env.TEAMAI_WIKI_DISABLED;
}
if (originalWikiEnabled !== undefined) {
process.env.TEAMAI_WIKI_ENABLED = originalWikiEnabled;
} else {
delete process.env.TEAMAI_WIKI_ENABLED;
}
});

it('returns true by default (no env vars, no config)', () => {
expect(resolveWikiEnabled(baseTeamConfig as any, baseLocalConfig as any)).toBe(true);
});

it('returns false when TEAMAI_WIKI_DISABLED=1', () => {
process.env.TEAMAI_WIKI_DISABLED = '1';
expect(resolveWikiEnabled(baseTeamConfig as any, baseLocalConfig as any)).toBe(false);
});

it('returns false when TEAMAI_WIKI_DISABLED=true', () => {
process.env.TEAMAI_WIKI_DISABLED = 'true';
expect(resolveWikiEnabled(baseTeamConfig as any, baseLocalConfig as any)).toBe(false);
});

it('returns false when TEAMAI_WIKI_ENABLED=0', () => {
process.env.TEAMAI_WIKI_ENABLED = '0';
expect(resolveWikiEnabled(baseTeamConfig as any, baseLocalConfig as any)).toBe(false);
});

it('returns false when TEAMAI_WIKI_ENABLED=false', () => {
process.env.TEAMAI_WIKI_ENABLED = 'false';
expect(resolveWikiEnabled(baseTeamConfig as any, baseLocalConfig as any)).toBe(false);
});

it('env var overrides local config wikiEnabled=true', () => {
process.env.TEAMAI_WIKI_DISABLED = '1';
const localConfig = { ...baseLocalConfig, wikiEnabled: true };
expect(resolveWikiEnabled(baseTeamConfig as any, localConfig as any)).toBe(false);
});

it('env var overrides team config wiki.enabled=true', () => {
process.env.TEAMAI_WIKI_DISABLED = '1';
const teamConfig = {
...baseTeamConfig,
sharing: { ...baseTeamConfig.sharing, wiki: { enabled: true } },
};
expect(resolveWikiEnabled(teamConfig as any, baseLocalConfig as any)).toBe(false);
});

it('returns true when TEAMAI_WIKI_DISABLED=0 (not a disable value)', () => {
process.env.TEAMAI_WIKI_DISABLED = '0';
expect(resolveWikiEnabled(baseTeamConfig as any, baseLocalConfig as any)).toBe(true);
});
});
10 changes: 8 additions & 2 deletions src/builtin-skills.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export const BUILTIN_SKILL_NAMES = new Set(['teamai-share-learnings', 'teamai-wi
* - Built-in skills directory doesn't exist (dev environment without build)
* - A tool's skills directory is not configured
*/
export async function deployBuiltinSkills(teamConfig: TeamaiConfig, localConfig?: LocalConfig): Promise<number> {
export async function deployBuiltinSkills(teamConfig: TeamaiConfig, localConfig?: LocalConfig, options?: { skipWiki?: boolean }): Promise<number> {
const builtinDir = getBuiltinSkillsDir();

if (!await pathExists(builtinDir)) {
Expand All @@ -76,6 +76,12 @@ export async function deployBuiltinSkills(teamConfig: TeamaiConfig, localConfig?

if (skillNames.length === 0) return 0;

// Skip teamai-wiki deployment when wiki feature is disabled
const filteredSkills = options?.skipWiki
? skillNames.filter(name => name !== 'teamai-wiki')
: skillNames;
if (filteredSkills.length === 0) return 0;

const baseDir = localConfig ? resolveBaseDir(localConfig) : (process.env.HOME ?? '');
let deployed = 0;

Expand All @@ -90,7 +96,7 @@ export async function deployBuiltinSkills(teamConfig: TeamaiConfig, localConfig?

const targetSkillsDir = path.join(baseDir, toolPath.skills);

for (const skillName of skillNames) {
for (const skillName of filteredSkills) {
const srcDir = path.join(builtinDir, skillName);
const destDir = path.join(targetSkillsDir, skillName);

Expand Down
10 changes: 7 additions & 3 deletions src/pull.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
TEAMAI_CLAUDEMD_END,
CultureFrontmatterSchema,
resolveBaseDir,
resolveWikiEnabled,
getTeamaiHome,
} from './types.js';
import type { CultureFrontmatter } from './types.js';
Expand Down Expand Up @@ -259,8 +260,11 @@ async function pullForScope(
const tagsConfig = await loadTagsConfig(localConfig.repo.localPath);
const subscribedTags = localConfig.subscribedTags;

// Step 2: Sync each resource type
const resourceTypes: ResourceType[] = ['skills', 'rules', 'docs', 'env', 'wiki'];
// Step 2: Sync each resource type (skip wiki when disabled)
const wikiEnabled = resolveWikiEnabled(freshConfig, localConfig);
const resourceTypes: ResourceType[] = wikiEnabled
? ['skills', 'rules', 'docs', 'env', 'wiki']
: ['skills', 'rules', 'docs', 'env'];
let totalSynced = 0;
let desiredSkillNames: Set<string> | null = null;
let knownRepoSkillNames: Set<string> | null = null;
Expand Down Expand Up @@ -555,7 +559,7 @@ async function pullForScope(
if (!options.dryRun) {
try {
const { deployBuiltinSkills } = await import('./builtin-skills.js');
const deployed = await deployBuiltinSkills(freshConfig, localConfig);
const deployed = await deployBuiltinSkills(freshConfig, localConfig, { skipWiki: !wikiEnabled });
if (deployed > 0) {
log.debug(`[${scopeLabel}] Deployed ${deployed} built-in skill(s)`);
}
Expand Down
6 changes: 5 additions & 1 deletion src/push.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { log, spinner } from './utils/logger.js';
import { getHandler } from './resources/index.js';
import { scanTeamRepoNamespaces } from './resources/skills.js';
import type { GlobalOptions, ResourceItem, ResourceType } from './types.js';
import { resolveWikiEnabled } from './types.js';
import { loadRolesManifest, resolveRoleResourceNamespaces } from './roles.js';
import { askQuestion, askSelection } from './utils/prompt.js';
import { pathExists } from './utils/fs.js';
Expand Down Expand Up @@ -137,7 +138,10 @@ export async function push(options: GlobalOptions & { all?: boolean; role?: stri

// Scan for pushable resources first, then resolve namespace for new skills only.
// Modified skills already carry their namespace from scanLocalForPush.
const pushableTypes: ResourceType[] = ['skills', 'rules', 'env', 'wiki'];
const wikiEnabled = resolveWikiEnabled(teamConfig, localConfig);
const pushableTypes: ResourceType[] = wikiEnabled
? ['skills', 'rules', 'env', 'wiki']
: ['skills', 'rules', 'env'];
const allItems: ResourceItem[] = [];

for (const type of pushableTypes) {
Expand Down
12 changes: 12 additions & 0 deletions src/status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
type AgentSkillsView,
} from './agent-skills.js';
import type { GlobalOptions, ResourceType } from './types.js';
import { resolveWikiEnabled } from './types.js';

export interface ListOptions extends GlobalOptions {
/** Where to look for resources: 'repo' (default for backwards compat),
Expand Down Expand Up @@ -100,6 +101,17 @@ export async function status(options: GlobalOptions): Promise<void> {
console.log(` ${type}: ${count}`);
}

// Wiki feature status
const wikiEnabled = resolveWikiEnabled(teamConfig, localConfig);
const envDisabled = process.env.TEAMAI_WIKI_DISABLED === '1' || process.env.TEAMAI_WIKI_DISABLED === 'true' || process.env.TEAMAI_WIKI_ENABLED === '0' || process.env.TEAMAI_WIKI_ENABLED === 'false';
const wikiSource = envDisabled
? 'disabled (TEAMAI_WIKI_DISABLED=1) — wiki routing handled by external plugin'
: localConfig.wikiEnabled !== undefined
? (wikiEnabled ? 'enabled (local override)' : 'disabled (local override)')
: (wikiEnabled ? 'enabled (team default)' : 'disabled (team config)');
console.log('');
log.info(`Wiki: ${wikiSource}`);

// Local pushable items
console.log('');
log.info('Local resources not yet pushed:');
Expand Down
Loading
Loading