@dpesch/mantisbt-mcp-server 1.10.2 → 1.10.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +34 -0
- package/README.de.md +2 -1
- package/README.md +2 -1
- package/dist/client.js +0 -15
- package/dist/tools/files.js +14 -15
- package/docs/cookbook.de.md +1 -1
- package/docs/cookbook.md +1 -1
- package/package.json +1 -1
- package/server.json +2 -2
- package/.gitea/PULL_REQUEST_TEMPLATE.md +0 -17
- package/.gitea/workflows/ci.yml +0 -95
- package/.github/workflows/ci.yml +0 -32
- package/scripts/hooks/pre-push.mjs +0 -220
- package/scripts/init.mjs +0 -95
- package/scripts/record-fixtures.ts +0 -160
- package/tests/cache.test.ts +0 -265
- package/tests/client.test.ts +0 -372
- package/tests/config.test.ts +0 -263
- package/tests/fixtures/get_current_user.json +0 -40
- package/tests/fixtures/get_issue.json +0 -133
- package/tests/fixtures/get_issue_enums.json +0 -67
- package/tests/fixtures/get_issue_fields_sample.json +0 -76
- package/tests/fixtures/get_project_categories.json +0 -157
- package/tests/fixtures/get_project_versions.json +0 -3
- package/tests/fixtures/get_project_versions_with_data.json +0 -52
- package/tests/fixtures/list_issues.json +0 -95
- package/tests/fixtures/list_projects.json +0 -65
- package/tests/helpers/mock-server.ts +0 -166
- package/tests/helpers/search-mocks.ts +0 -84
- package/tests/prompts/prompts.test.ts +0 -242
- package/tests/resources/resources.test.ts +0 -309
- package/tests/search/embedder.test.ts +0 -81
- package/tests/search/highlight.test.ts +0 -129
- package/tests/search/store.test.ts +0 -193
- package/tests/search/sync.test.ts +0 -249
- package/tests/search/tools.test.ts +0 -661
- package/tests/tools/config.test.ts +0 -212
- package/tests/tools/files.test.ts +0 -344
- package/tests/tools/issues.test.ts +0 -1180
- package/tests/tools/metadata.test.ts +0 -509
- package/tests/tools/monitors.test.ts +0 -101
- package/tests/tools/projects.test.ts +0 -338
- package/tests/tools/relationships.test.ts +0 -177
- package/tests/tools/string-coercion.test.ts +0 -317
- package/tests/tools/users.test.ts +0 -62
- package/tests/utils/date-filter.test.ts +0 -169
- package/tests/version-hint.test.ts +0 -230
- package/tsconfig.build.json +0 -8
- package/vitest.config.ts +0 -8
package/scripts/init.mjs
DELETED
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// Project setup script — run via: npm run init
|
|
3
|
-
//
|
|
4
|
-
// Steps:
|
|
5
|
-
// 1. Check Node.js version (requires >=18)
|
|
6
|
-
// 2. Install dependencies (npm install)
|
|
7
|
-
// 3. Install git hooks from scripts/hooks/ into .git/hooks/
|
|
8
|
-
// 4. Run typecheck to verify the setup
|
|
9
|
-
|
|
10
|
-
import { spawnSync } from 'node:child_process';
|
|
11
|
-
import { copyFileSync, chmodSync, existsSync, mkdirSync } from 'node:fs';
|
|
12
|
-
import { resolve, dirname, join } from 'node:path';
|
|
13
|
-
import { fileURLToPath } from 'node:url';
|
|
14
|
-
|
|
15
|
-
const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
16
|
-
// Auf Windows .cmd-Dateien via cmd.exe aufrufen — kein shell:true nötig, keine Deprecation-Warnung
|
|
17
|
-
const [npmBin, npmBaseArgs] = process.platform === 'win32'
|
|
18
|
-
? ['cmd.exe', ['/c', 'npm']]
|
|
19
|
-
: ['npm', []];
|
|
20
|
-
|
|
21
|
-
// ---------------------------------------------------------------------------
|
|
22
|
-
// 1. Node.js version check
|
|
23
|
-
// ---------------------------------------------------------------------------
|
|
24
|
-
|
|
25
|
-
const [major] = process.versions.node.split('.').map(Number);
|
|
26
|
-
if (major < 18) {
|
|
27
|
-
console.error(`✗ Node.js >= 18 required, found ${process.version}`);
|
|
28
|
-
process.exit(1);
|
|
29
|
-
}
|
|
30
|
-
console.log(`✓ Node.js ${process.version}`);
|
|
31
|
-
|
|
32
|
-
// ---------------------------------------------------------------------------
|
|
33
|
-
// 2. Install dependencies
|
|
34
|
-
// ---------------------------------------------------------------------------
|
|
35
|
-
|
|
36
|
-
console.log('\n→ Installing dependencies...');
|
|
37
|
-
const install = spawnSync(npmBin, [...npmBaseArgs, 'install'], {
|
|
38
|
-
stdio: 'inherit',
|
|
39
|
-
cwd: root,
|
|
40
|
-
});
|
|
41
|
-
if (install.status !== 0) {
|
|
42
|
-
console.error('✗ npm install failed');
|
|
43
|
-
process.exit(1);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// ---------------------------------------------------------------------------
|
|
47
|
-
// 3. Install git hooks
|
|
48
|
-
// ---------------------------------------------------------------------------
|
|
49
|
-
|
|
50
|
-
console.log('\n→ Installing git hooks...');
|
|
51
|
-
|
|
52
|
-
const hooksSourceDir = resolve(root, 'scripts/hooks');
|
|
53
|
-
const gitHooksDir = resolve(root, '.git/hooks');
|
|
54
|
-
|
|
55
|
-
if (!existsSync(gitHooksDir)) {
|
|
56
|
-
mkdirSync(gitHooksDir, { recursive: true });
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const hooks = ['pre-push'];
|
|
60
|
-
for (const hook of hooks) {
|
|
61
|
-
const src = resolve(hooksSourceDir, `${hook}.mjs`);
|
|
62
|
-
const dest = resolve(gitHooksDir, hook);
|
|
63
|
-
|
|
64
|
-
if (!existsSync(src)) {
|
|
65
|
-
console.warn(` ⚠ Hook source not found, skipping: scripts/hooks/${hook}.mjs`);
|
|
66
|
-
continue;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
copyFileSync(src, dest);
|
|
70
|
-
|
|
71
|
-
// chmod +x (no-op on Windows — git runs hooks directly via shebang)
|
|
72
|
-
try {
|
|
73
|
-
chmodSync(dest, 0o755);
|
|
74
|
-
} catch {
|
|
75
|
-
// Silently ignore on platforms that don't support chmod
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
console.log(` ✓ .git/hooks/${hook} installed`);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// ---------------------------------------------------------------------------
|
|
82
|
-
// 4. Typecheck
|
|
83
|
-
// ---------------------------------------------------------------------------
|
|
84
|
-
|
|
85
|
-
console.log('\n→ Running typecheck...');
|
|
86
|
-
const check = spawnSync(npmBin, [...npmBaseArgs, 'run', 'typecheck'], {
|
|
87
|
-
stdio: 'inherit',
|
|
88
|
-
cwd: root,
|
|
89
|
-
});
|
|
90
|
-
if (check.status !== 0) {
|
|
91
|
-
console.error('✗ Typecheck failed — check type errors above');
|
|
92
|
-
process.exit(1);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
console.log('\n✓ Setup complete. Happy hacking!');
|
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
import { writeFileSync, mkdirSync, readFileSync } from 'node:fs';
|
|
2
|
-
import { join, dirname } from 'node:path';
|
|
3
|
-
import { fileURLToPath } from 'node:url';
|
|
4
|
-
import { MantisClient } from '../src/client.js';
|
|
5
|
-
import { ISSUE_ENUM_OPTIONS } from '../src/constants.js';
|
|
6
|
-
|
|
7
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
-
const __dirname = dirname(__filename);
|
|
9
|
-
|
|
10
|
-
// ---------------------------------------------------------------------------
|
|
11
|
-
// .env.local laden (falls vorhanden)
|
|
12
|
-
// ---------------------------------------------------------------------------
|
|
13
|
-
|
|
14
|
-
try {
|
|
15
|
-
const envPath = join(__dirname, '..', '.env.local');
|
|
16
|
-
const lines = readFileSync(envPath, 'utf-8').split('\n');
|
|
17
|
-
for (const line of lines) {
|
|
18
|
-
const match = line.match(/^([^#=\s][^=]*)=(.*)$/);
|
|
19
|
-
if (match) {
|
|
20
|
-
const key = match[1].trim();
|
|
21
|
-
const value = match[2].trim().replace(/^["']|["']$/g, '');
|
|
22
|
-
if (!process.env[key]) process.env[key] = value;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
} catch {
|
|
26
|
-
// .env.local nicht vorhanden – ENV-Vars direkt nutzen
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// ---------------------------------------------------------------------------
|
|
30
|
-
// Config aus ENV
|
|
31
|
-
// ---------------------------------------------------------------------------
|
|
32
|
-
|
|
33
|
-
const baseUrlRaw = process.env['MANTIS_BASE_URL'];
|
|
34
|
-
const apiKey = process.env['MANTIS_API_KEY'];
|
|
35
|
-
|
|
36
|
-
if (!baseUrlRaw || !apiKey) {
|
|
37
|
-
console.error('Error: MANTIS_BASE_URL and MANTIS_API_KEY environment variables must be set.');
|
|
38
|
-
process.exit(1);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const baseUrl = baseUrlRaw.replace(/\/$/, '');
|
|
42
|
-
|
|
43
|
-
// ---------------------------------------------------------------------------
|
|
44
|
-
// Fixtures-Verzeichnis
|
|
45
|
-
// ---------------------------------------------------------------------------
|
|
46
|
-
|
|
47
|
-
const fixturesDir = join(__dirname, '..', 'tests', 'fixtures', 'recorded');
|
|
48
|
-
mkdirSync(fixturesDir, { recursive: true });
|
|
49
|
-
|
|
50
|
-
function saveFixture(filename: string, data: unknown): void {
|
|
51
|
-
const filePath = join(fixturesDir, filename);
|
|
52
|
-
writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n', 'utf-8');
|
|
53
|
-
console.log(`Saved: ${filePath}`);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// ---------------------------------------------------------------------------
|
|
57
|
-
// Client instanziieren
|
|
58
|
-
// ---------------------------------------------------------------------------
|
|
59
|
-
|
|
60
|
-
const client = new MantisClient(baseUrl, apiKey);
|
|
61
|
-
|
|
62
|
-
// ---------------------------------------------------------------------------
|
|
63
|
-
// Fixtures aufzeichnen
|
|
64
|
-
// ---------------------------------------------------------------------------
|
|
65
|
-
|
|
66
|
-
async function recordFixtures(): Promise<void> {
|
|
67
|
-
// GET projects
|
|
68
|
-
let firstProjectId: number | undefined;
|
|
69
|
-
let projectIdWithVersions: number | undefined;
|
|
70
|
-
try {
|
|
71
|
-
const projectsResult = await client.get<{ projects: Array<{ id: number; name: string; versions?: unknown[] }> }>('projects');
|
|
72
|
-
saveFixture('list_projects.json', projectsResult);
|
|
73
|
-
if (Array.isArray(projectsResult.projects) && projectsResult.projects.length > 0) {
|
|
74
|
-
firstProjectId = projectsResult.projects[0]?.id;
|
|
75
|
-
// Erstes Projekt mit nicht-leeren Versionen bevorzugen
|
|
76
|
-
projectIdWithVersions = projectsResult.projects.find(
|
|
77
|
-
(p) => Array.isArray(p.versions) && p.versions.length > 0,
|
|
78
|
-
)?.id ?? firstProjectId;
|
|
79
|
-
}
|
|
80
|
-
} catch (err) {
|
|
81
|
-
console.error('Failed to fetch projects:', err instanceof Error ? err.message : String(err));
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// GET users/me
|
|
85
|
-
try {
|
|
86
|
-
const userResult = await client.get<unknown>('users/me');
|
|
87
|
-
saveFixture('get_current_user.json', userResult);
|
|
88
|
-
} catch (err) {
|
|
89
|
-
console.error('Failed to fetch users/me:', err instanceof Error ? err.message : String(err));
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// GET issues?page=1&page_size=3
|
|
93
|
-
let firstIssueId: number | undefined;
|
|
94
|
-
try {
|
|
95
|
-
const issuesResult = await client.get<{ issues: Array<{ id: number }> }>('issues', {
|
|
96
|
-
page: 1,
|
|
97
|
-
page_size: 3,
|
|
98
|
-
});
|
|
99
|
-
saveFixture('list_issues.json', issuesResult);
|
|
100
|
-
if (Array.isArray(issuesResult.issues) && issuesResult.issues.length > 0) {
|
|
101
|
-
firstIssueId = issuesResult.issues[0]?.id;
|
|
102
|
-
}
|
|
103
|
-
} catch (err) {
|
|
104
|
-
console.error('Failed to fetch issues:', err instanceof Error ? err.message : String(err));
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// GET issues?page=1&page_size=1 — single issue sample for get_issue_fields field discovery
|
|
108
|
-
try {
|
|
109
|
-
const sampleResult = await client.get<{ issues: unknown[] }>('issues', {
|
|
110
|
-
page: 1,
|
|
111
|
-
page_size: 1,
|
|
112
|
-
});
|
|
113
|
-
saveFixture('get_issue_fields_sample.json', sampleResult);
|
|
114
|
-
} catch (err) {
|
|
115
|
-
console.error('Failed to fetch issues sample:', err instanceof Error ? err.message : String(err));
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// GET issues/{id}
|
|
119
|
-
if (firstIssueId !== undefined) {
|
|
120
|
-
try {
|
|
121
|
-
const issueResult = await client.get<unknown>(`issues/${firstIssueId}`);
|
|
122
|
-
saveFixture('get_issue.json', issueResult);
|
|
123
|
-
} catch (err) {
|
|
124
|
-
console.error(`Failed to fetch issues/${firstIssueId}:`, err instanceof Error ? err.message : String(err));
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// GET config — enum values for severity, status, priority, resolution, reproducibility
|
|
129
|
-
try {
|
|
130
|
-
const enumParams: Record<string, string> = {};
|
|
131
|
-
ISSUE_ENUM_OPTIONS.forEach((opt, i) => { enumParams[`option[${i}]`] = opt; });
|
|
132
|
-
const configResult = await client.get<unknown>('config', enumParams);
|
|
133
|
-
saveFixture('get_issue_enums.json', configResult);
|
|
134
|
-
} catch (err) {
|
|
135
|
-
console.error('Failed to fetch config enums:', err instanceof Error ? err.message : String(err));
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// GET projects/{id}/versions + categories
|
|
139
|
-
if (firstProjectId !== undefined) {
|
|
140
|
-
const versionProjectId = projectIdWithVersions ?? firstProjectId;
|
|
141
|
-
try {
|
|
142
|
-
const versionsResult = await client.get<unknown>(`projects/${versionProjectId}/versions`, { obsolete: 1, inherit: 1 });
|
|
143
|
-
saveFixture('get_project_versions.json', versionsResult);
|
|
144
|
-
} catch (err) {
|
|
145
|
-
console.error(`Failed to fetch projects/${versionProjectId}/versions:`, err instanceof Error ? err.message : String(err));
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
try {
|
|
149
|
-
const projectResult = await client.get<{ projects: Array<{ categories?: unknown[] }> }>(`projects/${firstProjectId}`);
|
|
150
|
-
saveFixture('get_project_categories.json', { categories: projectResult.projects?.[0]?.categories ?? [] });
|
|
151
|
-
} catch (err) {
|
|
152
|
-
console.error(`Failed to fetch projects/${firstProjectId} (categories):`, err instanceof Error ? err.message : String(err));
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
recordFixtures().catch((err) => {
|
|
158
|
-
console.error('Unexpected error:', err);
|
|
159
|
-
process.exit(1);
|
|
160
|
-
});
|
package/tests/cache.test.ts
DELETED
|
@@ -1,265 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
-
|
|
3
|
-
// vi.mock must be at module top level — vitest hoists it automatically
|
|
4
|
-
vi.mock('node:fs/promises');
|
|
5
|
-
|
|
6
|
-
import { readFile, writeFile, unlink, mkdir } from 'node:fs/promises';
|
|
7
|
-
import { MetadataCache, type CachedMetadata } from '../src/cache.js';
|
|
8
|
-
|
|
9
|
-
// ---------------------------------------------------------------------------
|
|
10
|
-
// Helpers
|
|
11
|
-
// ---------------------------------------------------------------------------
|
|
12
|
-
|
|
13
|
-
const CACHE_DIR = '/tmp/test-cache';
|
|
14
|
-
const TTL = 3600; // 1 hour in seconds
|
|
15
|
-
|
|
16
|
-
function makeCache(): MetadataCache {
|
|
17
|
-
return new MetadataCache(CACHE_DIR, TTL);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function makeSampleMetadata(): CachedMetadata {
|
|
21
|
-
return {
|
|
22
|
-
timestamp: Date.now(),
|
|
23
|
-
projects: [{ id: 1, name: 'Test Project' }],
|
|
24
|
-
byProject: {},
|
|
25
|
-
tags: [],
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// ---------------------------------------------------------------------------
|
|
30
|
-
// Setup
|
|
31
|
-
// ---------------------------------------------------------------------------
|
|
32
|
-
|
|
33
|
-
beforeEach(() => {
|
|
34
|
-
vi.resetAllMocks();
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
// ---------------------------------------------------------------------------
|
|
38
|
-
// isValid()
|
|
39
|
-
// ---------------------------------------------------------------------------
|
|
40
|
-
|
|
41
|
-
describe('MetadataCache – isValid()', () => {
|
|
42
|
-
it('returns false when file does not exist (readFile throws)', async () => {
|
|
43
|
-
vi.mocked(readFile).mockRejectedValue(new Error('ENOENT'));
|
|
44
|
-
|
|
45
|
-
const cache = makeCache();
|
|
46
|
-
await expect(cache.isValid()).resolves.toBe(false);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it('returns true when file is fresh (within TTL)', async () => {
|
|
50
|
-
const file = { timestamp: Date.now(), data: makeSampleMetadata() };
|
|
51
|
-
vi.mocked(readFile).mockResolvedValue(JSON.stringify(file) as any);
|
|
52
|
-
|
|
53
|
-
const cache = makeCache();
|
|
54
|
-
await expect(cache.isValid()).resolves.toBe(true);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it('returns false when file has expired (timestamp older than TTL)', async () => {
|
|
58
|
-
const expiredTimestamp = Date.now() - (TTL + 1) * 1000;
|
|
59
|
-
const file = { timestamp: expiredTimestamp, data: makeSampleMetadata() };
|
|
60
|
-
vi.mocked(readFile).mockResolvedValue(JSON.stringify(file) as any);
|
|
61
|
-
|
|
62
|
-
const cache = makeCache();
|
|
63
|
-
await expect(cache.isValid()).resolves.toBe(false);
|
|
64
|
-
});
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
// ---------------------------------------------------------------------------
|
|
68
|
-
// load()
|
|
69
|
-
// ---------------------------------------------------------------------------
|
|
70
|
-
|
|
71
|
-
describe('MetadataCache – load()', () => {
|
|
72
|
-
it('returns null when file does not exist', async () => {
|
|
73
|
-
vi.mocked(readFile).mockRejectedValue(new Error('ENOENT'));
|
|
74
|
-
|
|
75
|
-
const cache = makeCache();
|
|
76
|
-
await expect(cache.load()).resolves.toBeNull();
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it('returns CachedMetadata when file exists', async () => {
|
|
80
|
-
const metadata = makeSampleMetadata();
|
|
81
|
-
const file = { timestamp: Date.now(), data: metadata };
|
|
82
|
-
vi.mocked(readFile).mockResolvedValue(JSON.stringify(file) as any);
|
|
83
|
-
|
|
84
|
-
const cache = makeCache();
|
|
85
|
-
const result = await cache.load();
|
|
86
|
-
expect(result).toEqual(metadata);
|
|
87
|
-
});
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
// ---------------------------------------------------------------------------
|
|
91
|
-
// save()
|
|
92
|
-
// ---------------------------------------------------------------------------
|
|
93
|
-
|
|
94
|
-
describe('MetadataCache – save()', () => {
|
|
95
|
-
it('calls mkdir with recursive:true and writeFile with JSON content', async () => {
|
|
96
|
-
vi.mocked(mkdir).mockResolvedValue(undefined);
|
|
97
|
-
vi.mocked(writeFile).mockResolvedValue(undefined);
|
|
98
|
-
|
|
99
|
-
const cache = makeCache();
|
|
100
|
-
const metadata = makeSampleMetadata();
|
|
101
|
-
await cache.save(metadata);
|
|
102
|
-
|
|
103
|
-
expect(mkdir).toHaveBeenCalledWith(CACHE_DIR, { recursive: true });
|
|
104
|
-
expect(writeFile).toHaveBeenCalledOnce();
|
|
105
|
-
|
|
106
|
-
// Verify the written JSON contains our data
|
|
107
|
-
const writtenContent = vi.mocked(writeFile).mock.calls[0][1] as string;
|
|
108
|
-
const parsed = JSON.parse(writtenContent) as { timestamp: number; data: CachedMetadata };
|
|
109
|
-
expect(parsed.data).toEqual(metadata);
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
it('writes a timestamp to the cache file', async () => {
|
|
113
|
-
vi.mocked(mkdir).mockResolvedValue(undefined);
|
|
114
|
-
vi.mocked(writeFile).mockResolvedValue(undefined);
|
|
115
|
-
|
|
116
|
-
const before = Date.now();
|
|
117
|
-
const cache = makeCache();
|
|
118
|
-
await cache.save(makeSampleMetadata());
|
|
119
|
-
const after = Date.now();
|
|
120
|
-
|
|
121
|
-
const writtenContent = vi.mocked(writeFile).mock.calls[0][1] as string;
|
|
122
|
-
const parsed = JSON.parse(writtenContent) as { timestamp: number };
|
|
123
|
-
expect(parsed.timestamp).toBeGreaterThanOrEqual(before);
|
|
124
|
-
expect(parsed.timestamp).toBeLessThanOrEqual(after);
|
|
125
|
-
});
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
// ---------------------------------------------------------------------------
|
|
129
|
-
// invalidate()
|
|
130
|
-
// ---------------------------------------------------------------------------
|
|
131
|
-
|
|
132
|
-
describe('MetadataCache – invalidate()', () => {
|
|
133
|
-
it('calls unlink on the cache file', async () => {
|
|
134
|
-
vi.mocked(unlink).mockResolvedValue(undefined);
|
|
135
|
-
|
|
136
|
-
const cache = makeCache();
|
|
137
|
-
await cache.invalidate();
|
|
138
|
-
|
|
139
|
-
expect(unlink).toHaveBeenCalledOnce();
|
|
140
|
-
const calledPath = vi.mocked(unlink).mock.calls[0][0] as string;
|
|
141
|
-
expect(calledPath).toContain('metadata.json');
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
it('does not throw when file does not exist (unlink rejects)', async () => {
|
|
145
|
-
vi.mocked(unlink).mockRejectedValue(new Error('ENOENT'));
|
|
146
|
-
|
|
147
|
-
const cache = makeCache();
|
|
148
|
-
await expect(cache.invalidate()).resolves.toBeUndefined();
|
|
149
|
-
});
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
// ---------------------------------------------------------------------------
|
|
153
|
-
// loadIfValid()
|
|
154
|
-
// ---------------------------------------------------------------------------
|
|
155
|
-
|
|
156
|
-
describe('MetadataCache – loadIfValid()', () => {
|
|
157
|
-
it('returns null when file does not exist', async () => {
|
|
158
|
-
vi.mocked(readFile).mockRejectedValue(new Error('ENOENT'));
|
|
159
|
-
|
|
160
|
-
const cache = makeCache();
|
|
161
|
-
await expect(cache.loadIfValid()).resolves.toBeNull();
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
it('returns data when file is fresh (within TTL)', async () => {
|
|
165
|
-
const metadata = makeSampleMetadata();
|
|
166
|
-
const file = { timestamp: Date.now(), data: metadata };
|
|
167
|
-
vi.mocked(readFile).mockResolvedValue(JSON.stringify(file) as any);
|
|
168
|
-
|
|
169
|
-
const cache = makeCache();
|
|
170
|
-
const result = await cache.loadIfValid();
|
|
171
|
-
expect(result).toEqual(metadata);
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
it('returns null when file has expired', async () => {
|
|
175
|
-
const expiredTimestamp = Date.now() - (TTL + 1) * 1000;
|
|
176
|
-
const metadata = makeSampleMetadata();
|
|
177
|
-
const file = { timestamp: expiredTimestamp, data: metadata };
|
|
178
|
-
vi.mocked(readFile).mockResolvedValue(JSON.stringify(file) as any);
|
|
179
|
-
|
|
180
|
-
const cache = makeCache();
|
|
181
|
-
await expect(cache.loadIfValid()).resolves.toBeNull();
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
it('reads from metadata.json path', async () => {
|
|
185
|
-
vi.mocked(readFile).mockRejectedValue(new Error('ENOENT'));
|
|
186
|
-
|
|
187
|
-
const cache = makeCache();
|
|
188
|
-
await cache.loadIfValid();
|
|
189
|
-
|
|
190
|
-
expect(readFile).toHaveBeenCalledOnce();
|
|
191
|
-
const calledPath = vi.mocked(readFile).mock.calls[0][0] as string;
|
|
192
|
-
expect(calledPath).toContain('metadata.json');
|
|
193
|
-
expect(calledPath).not.toContain('issue_fields.json');
|
|
194
|
-
});
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
// ---------------------------------------------------------------------------
|
|
198
|
-
// loadIssueFields()
|
|
199
|
-
// ---------------------------------------------------------------------------
|
|
200
|
-
|
|
201
|
-
describe('MetadataCache – loadIssueFields()', () => {
|
|
202
|
-
it('returns null when file does not exist', async () => {
|
|
203
|
-
vi.mocked(readFile).mockRejectedValue(new Error('ENOENT'));
|
|
204
|
-
|
|
205
|
-
const cache = makeCache();
|
|
206
|
-
await expect(cache.loadIssueFields()).resolves.toBeNull();
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
it('returns fields array when file is fresh', async () => {
|
|
210
|
-
const fields = ['id', 'summary', 'status'];
|
|
211
|
-
const file = { timestamp: Date.now(), fields };
|
|
212
|
-
vi.mocked(readFile).mockResolvedValue(JSON.stringify(file) as any);
|
|
213
|
-
|
|
214
|
-
const cache = makeCache();
|
|
215
|
-
const result = await cache.loadIssueFields();
|
|
216
|
-
expect(result).toEqual(fields);
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
it('returns null when file has expired', async () => {
|
|
220
|
-
const expiredTimestamp = Date.now() - (TTL + 1) * 1000;
|
|
221
|
-
const file = { timestamp: expiredTimestamp, fields: ['id', 'summary'] };
|
|
222
|
-
vi.mocked(readFile).mockResolvedValue(JSON.stringify(file) as any);
|
|
223
|
-
|
|
224
|
-
const cache = makeCache();
|
|
225
|
-
await expect(cache.loadIssueFields()).resolves.toBeNull();
|
|
226
|
-
});
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
// ---------------------------------------------------------------------------
|
|
230
|
-
// saveIssueFields()
|
|
231
|
-
// ---------------------------------------------------------------------------
|
|
232
|
-
|
|
233
|
-
describe('MetadataCache – saveIssueFields()', () => {
|
|
234
|
-
it('calls mkdir with recursive:true and writes to issue_fields.json', async () => {
|
|
235
|
-
vi.mocked(mkdir).mockResolvedValue(undefined);
|
|
236
|
-
vi.mocked(writeFile).mockResolvedValue(undefined);
|
|
237
|
-
|
|
238
|
-
const cache = makeCache();
|
|
239
|
-
const fields = ['id', 'summary', 'status', 'priority'];
|
|
240
|
-
await cache.saveIssueFields(fields);
|
|
241
|
-
|
|
242
|
-
expect(mkdir).toHaveBeenCalledWith(CACHE_DIR, { recursive: true });
|
|
243
|
-
expect(writeFile).toHaveBeenCalledOnce();
|
|
244
|
-
|
|
245
|
-
const calledPath = vi.mocked(writeFile).mock.calls[0][0] as string;
|
|
246
|
-
expect(calledPath).toContain('issue_fields.json');
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
it('written JSON contains the fields array and a timestamp', async () => {
|
|
250
|
-
vi.mocked(mkdir).mockResolvedValue(undefined);
|
|
251
|
-
vi.mocked(writeFile).mockResolvedValue(undefined);
|
|
252
|
-
|
|
253
|
-
const cache = makeCache();
|
|
254
|
-
const fields = ['id', 'summary', 'status'];
|
|
255
|
-
const before = Date.now();
|
|
256
|
-
await cache.saveIssueFields(fields);
|
|
257
|
-
const after = Date.now();
|
|
258
|
-
|
|
259
|
-
const writtenContent = vi.mocked(writeFile).mock.calls[0][1] as string;
|
|
260
|
-
const parsed = JSON.parse(writtenContent) as { timestamp: number; fields: string[] };
|
|
261
|
-
expect(parsed.fields).toEqual(fields);
|
|
262
|
-
expect(parsed.timestamp).toBeGreaterThanOrEqual(before);
|
|
263
|
-
expect(parsed.timestamp).toBeLessThanOrEqual(after);
|
|
264
|
-
});
|
|
265
|
-
});
|