0d3cb498a3
Deploy local.promptfoo.app / Deploy to Cloudflare Pages (push) Waiting to run
Test and Publish Multi-arch Docker Image / test (push) Waiting to run
Test and Publish Multi-arch Docker Image / build-docker-and-push-digests (map[digest-suffix:linux-amd64 platform:linux/amd64 runner:ubuntu-latest]) (push) Blocked by required conditions
Test and Publish Multi-arch Docker Image / build-docker-and-push-digests (map[digest-suffix:linux-arm64 platform:linux/arm64 runner:ubuntu-24.04-arm]) (push) Blocked by required conditions
Test and Publish Multi-arch Docker Image / merge-docker-digests (push) Blocked by required conditions
Test and Publish Multi-arch Docker Image / Attest Multi-arch Image (push) Blocked by required conditions
Validate Renovate Config / Validate Renovate Configuration (push) Waiting to run
CI / Shell Format Check (push) Has been cancelled
CI / Check Ruby (3.4) (push) Has been cancelled
CI / CI Config (push) Has been cancelled
CI / Test on Node ${{ matrix.node }} and ${{ matrix.os }}${{ matrix.shard && format(' (shard {0}/3)', matrix.shard) || '' }} (push) Has been cancelled
CI / Build on Node ${{ matrix.node }} (push) Has been cancelled
CI / Style Check (push) Has been cancelled
CI / Generate Assets (push) Has been cancelled
CI / Check Python (3.14) (push) Has been cancelled
CI / Check Python (3.9) (push) Has been cancelled
CI / Build Docs (push) Has been cancelled
CI / Code Scan Action (push) Has been cancelled
CI / Site tests (push) Has been cancelled
CI / webui tests (push) Has been cancelled
CI / Run Integration Tests (push) Has been cancelled
CI / Run Smoke Tests (push) Has been cancelled
CI / Go Tests (push) Has been cancelled
CI / Share Test (push) Has been cancelled
CI / Redteam (Production API) (push) Has been cancelled
CI / Redteam (Staging API) (push) Has been cancelled
CI / GitHub Actions Lint (push) Has been cancelled
CI / Check Ruby (3.0) (push) Has been cancelled
release-please / release-please (push) Has been cancelled
release-please / build (push) Has been cancelled
release-please / publish-npm (push) Has been cancelled
release-please / publish-npm-backfill (push) Has been cancelled
release-please / docker (push) Has been cancelled
release-please / publish-code-scan-action (push) Has been cancelled
release-please / attest-code-scan-action (push) Has been cancelled
159 lines
6.1 KiB
TypeScript
159 lines
6.1 KiB
TypeScript
import path from 'path';
|
|
|
|
import { describe, expect, it } from 'vitest';
|
|
import {
|
|
buildCodexSkillMetadata,
|
|
type CodexCommandSkillItemAdapter,
|
|
extractCodexSkillPathCandidates,
|
|
getCodexSkillMetadataFields,
|
|
getCodexSkillRootPrefixes,
|
|
} from '../../src/providers/openai/codexSkillMetadata';
|
|
|
|
describe('codexSkillMetadata', () => {
|
|
describe('getCodexSkillRootPrefixes', () => {
|
|
it('always includes the system codex root', () => {
|
|
expect(getCodexSkillRootPrefixes({})).toEqual(['/etc/codex']);
|
|
});
|
|
|
|
it('normalizes backslashes and strips trailing slashes', () => {
|
|
const prefixes = getCodexSkillRootPrefixes({ codexHome: 'C:\\codex\\home\\\\' });
|
|
expect(prefixes).toContain('C:/codex/home');
|
|
});
|
|
|
|
it('deduplicates equal prefixes', () => {
|
|
const prefixes = getCodexSkillRootPrefixes({ codexHome: '/etc/codex' });
|
|
expect(prefixes).toEqual(['/etc/codex']);
|
|
});
|
|
|
|
it('adds the working dir .agents root and ignores an orphan git root without a working dir', () => {
|
|
const workingDir = path.resolve('/repo/project');
|
|
const withWorkingDir = getCodexSkillRootPrefixes({
|
|
workingDir,
|
|
gitRepositoryRoot: '/repo',
|
|
});
|
|
// The working dir is resolved to an absolute path, so derive the expected
|
|
// prefix the same way rather than hard-coding a POSIX-only string.
|
|
expect(withWorkingDir).toContain(path.posix.join(workingDir.replace(/\\/g, '/'), '.agents'));
|
|
expect(withWorkingDir).toContain('/repo/.agents');
|
|
|
|
const orphanGitRoot = getCodexSkillRootPrefixes({ gitRepositoryRoot: '/repo' });
|
|
expect(orphanGitRoot).toEqual(['/etc/codex']);
|
|
});
|
|
|
|
it('adds the home .codex root', () => {
|
|
expect(getCodexSkillRootPrefixes({ homeDir: '/home/user' })).toContain('/home/user/.codex');
|
|
});
|
|
});
|
|
|
|
describe('extractCodexSkillPathCandidates', () => {
|
|
it('detects repo-local SKILL.md paths', () => {
|
|
expect(extractCodexSkillPathCandidates('.agents/skills/repo-skill/SKILL.md --help')).toEqual([
|
|
{ name: 'repo-skill', path: '.agents/skills/repo-skill/SKILL.md' },
|
|
]);
|
|
});
|
|
|
|
it('strips wrapping punctuation from tokens', () => {
|
|
expect(extractCodexSkillPathCandidates('`.agents/skills/repo-skill/SKILL.md`')).toEqual([
|
|
{ name: 'repo-skill', path: '.agents/skills/repo-skill/SKILL.md' },
|
|
]);
|
|
expect(extractCodexSkillPathCandidates('(.agents/skills/repo-skill/SKILL.md),')).toEqual([
|
|
{ name: 'repo-skill', path: '.agents/skills/repo-skill/SKILL.md' },
|
|
]);
|
|
});
|
|
|
|
it('rejects wildcard skill segments as invalid names', () => {
|
|
expect(extractCodexSkillPathCandidates('.agents/skills/*/SKILL.md')).toEqual([]);
|
|
});
|
|
|
|
it('rejects paths outside any known skill root', () => {
|
|
expect(
|
|
extractCodexSkillPathCandidates('/tmp/unrelated/skills/external/SKILL.md', [
|
|
'/repo/.agents',
|
|
]),
|
|
).toEqual([]);
|
|
});
|
|
|
|
it('detects skills under a custom skill root prefix', () => {
|
|
expect(
|
|
extractCodexSkillPathCandidates('/repo/.agents/skills/custom/SKILL.md', ['/repo/.agents']),
|
|
).toEqual([{ name: 'custom', path: '/repo/.agents/skills/custom/SKILL.md' }]);
|
|
});
|
|
|
|
it('does not treat a string-prefix-but-not-path-segment root as a match', () => {
|
|
// '/etc/codex-extra/...' must not match the '/etc/codex' prefix.
|
|
expect(
|
|
extractCodexSkillPathCandidates('/etc/codex-extra/skills/foo/SKILL.md', ['/etc/codex']),
|
|
).toEqual([]);
|
|
});
|
|
|
|
it('deduplicates a path repeated within one command', () => {
|
|
expect(
|
|
extractCodexSkillPathCandidates(
|
|
'.agents/skills/repo-skill/SKILL.md .agents/skills/repo-skill/SKILL.md',
|
|
),
|
|
).toEqual([{ name: 'repo-skill', path: '.agents/skills/repo-skill/SKILL.md' }]);
|
|
});
|
|
});
|
|
|
|
describe('buildCodexSkillMetadata', () => {
|
|
const adapter: CodexCommandSkillItemAdapter = {
|
|
getCommand: (item) => (item as { command?: string })?.command,
|
|
isSuccessfulCommand: (item) => (item as { ok?: boolean })?.ok === true,
|
|
};
|
|
|
|
it('returns undefined for non-array or empty input', () => {
|
|
expect(buildCodexSkillMetadata(undefined, [], adapter)).toBeUndefined();
|
|
expect(buildCodexSkillMetadata('not-an-array', [], adapter)).toBeUndefined();
|
|
expect(buildCodexSkillMetadata([], [], adapter)).toBeUndefined();
|
|
});
|
|
|
|
it('returns undefined when no command references a skill', () => {
|
|
expect(
|
|
buildCodexSkillMetadata([{ command: 'ls -la', ok: true }], [], adapter),
|
|
).toBeUndefined();
|
|
});
|
|
|
|
it('records a successful skill call as both attempted and confirmed', () => {
|
|
const metadata = buildCodexSkillMetadata(
|
|
[{ command: '.agents/skills/repo-skill/SKILL.md', ok: true }],
|
|
[],
|
|
adapter,
|
|
);
|
|
expect(metadata?.skillCalls).toEqual([
|
|
{ name: 'repo-skill', path: '.agents/skills/repo-skill/SKILL.md', source: 'heuristic' },
|
|
]);
|
|
expect(metadata?.attemptedSkillCalls).toEqual(metadata?.skillCalls);
|
|
});
|
|
|
|
it('records a failed skill call as attempted only', () => {
|
|
const metadata = buildCodexSkillMetadata(
|
|
[{ command: '.agents/skills/repo-skill/SKILL.md', ok: false }],
|
|
[],
|
|
adapter,
|
|
);
|
|
expect(metadata?.attemptedSkillCalls).toHaveLength(1);
|
|
expect(metadata?.skillCalls).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
describe('getCodexSkillMetadataFields', () => {
|
|
const skill = { name: 's', path: '.agents/skills/s/SKILL.md', source: 'heuristic' as const };
|
|
|
|
it('emits skillCalls only when attempted equals confirmed', () => {
|
|
expect(
|
|
getCodexSkillMetadataFields({ attemptedSkillCalls: [skill], skillCalls: [skill] }),
|
|
).toEqual({ skillCalls: [skill] });
|
|
});
|
|
|
|
it('emits attemptedSkillCalls only when it exceeds confirmed calls', () => {
|
|
expect(getCodexSkillMetadataFields({ attemptedSkillCalls: [skill], skillCalls: [] })).toEqual(
|
|
{ attemptedSkillCalls: [skill] },
|
|
);
|
|
});
|
|
|
|
it('omits empty skillCalls', () => {
|
|
expect(getCodexSkillMetadataFields({ attemptedSkillCalls: [], skillCalls: [] })).toEqual({});
|
|
});
|
|
});
|
|
});
|