@vanshbhardwaj/worklog 1.0.0
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/.github/ISSUE_TEMPLATE/bug_report.md +28 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +19 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +19 -0
- package/.github/workflows/release.yml +63 -0
- package/.github/workflows/validate.yml +103 -0
- package/.prettierrc +8 -0
- package/ARCHITECTURE.md +82 -0
- package/CODE_OF_CONDUCT.md +65 -0
- package/CONTRIBUTING.md +74 -0
- package/INSTALLATION.md +98 -0
- package/LICENSE +21 -0
- package/README.md +246 -0
- package/SECURITY.md +18 -0
- package/benchmarks/benchmark.ts +149 -0
- package/benchmarks/vitest.bench.config.ts +9 -0
- package/dist/database/migrations/001_initial.sql +60 -0
- package/dist/database/migrations/002_relational_tables.sql +22 -0
- package/dist/index.cjs +31007 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1873 -0
- package/dist/index.js.map +1 -0
- package/eslint.config.js +38 -0
- package/package.json +49 -0
- package/pnpm-workspace.yaml +6 -0
- package/scripts/build-bin.js +75 -0
- package/sea-config.json +5 -0
- package/src/cli/commands/add.ts +176 -0
- package/src/cli/commands/continue.ts +80 -0
- package/src/cli/commands/dashboard.ts +30 -0
- package/src/cli/commands/export.ts +72 -0
- package/src/cli/commands/init.ts +52 -0
- package/src/cli/commands/list.ts +33 -0
- package/src/cli/commands/search.ts +33 -0
- package/src/cli/commands/show.ts +42 -0
- package/src/cli/commands/stats.ts +23 -0
- package/src/cli/commands/today.ts +27 -0
- package/src/cli/commands/week.ts +93 -0
- package/src/cli/index.ts +294 -0
- package/src/cli/ui.ts +323 -0
- package/src/core/config.ts +69 -0
- package/src/core/discovery.ts +38 -0
- package/src/core/logger.ts +53 -0
- package/src/database/connection.ts +95 -0
- package/src/database/migration-data.ts +71 -0
- package/src/database/migrations/001_initial.sql +60 -0
- package/src/database/migrations/002_relational_tables.sql +22 -0
- package/src/database/migrations.ts +65 -0
- package/src/errors/index.ts +51 -0
- package/src/exporters/csv.ts +59 -0
- package/src/exporters/json.ts +8 -0
- package/src/exporters/markdown.ts +71 -0
- package/src/models/work-unit.ts +38 -0
- package/src/repositories/work-unit.ts +466 -0
- package/src/services/work-unit.ts +131 -0
- package/src/tests/cli/__snapshots__/cli-productivity.test.ts.snap +37 -0
- package/src/tests/cli/__snapshots__/cli.test.ts.snap +20 -0
- package/src/tests/cli/cli-productivity.test.ts +167 -0
- package/src/tests/cli/cli.test.ts +171 -0
- package/src/tests/core/config.test.ts +53 -0
- package/src/tests/core/discovery.test.ts +46 -0
- package/src/tests/database/migrations.test.ts +75 -0
- package/src/tests/repositories/work-unit.test.ts +243 -0
- package/src/tests/services/work-unit.test.ts +87 -0
- package/src/validators/work-unit.ts +63 -0
- package/tsconfig.json +16 -0
- package/tsup.config.ts +50 -0
- package/vitest.config.ts +8 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { execSync } from 'child_process';
|
|
5
|
+
|
|
6
|
+
interface CLIResult {
|
|
7
|
+
stdout: string;
|
|
8
|
+
stderr: string;
|
|
9
|
+
exitCode: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function runCLI(args: string[], cwd: string): CLIResult {
|
|
13
|
+
const cliPath = path.resolve(process.cwd(), 'dist', 'index.js');
|
|
14
|
+
try {
|
|
15
|
+
const stdout = execSync(`node ${cliPath} ${args.join(' ')}`, {
|
|
16
|
+
cwd,
|
|
17
|
+
stdio: 'pipe',
|
|
18
|
+
});
|
|
19
|
+
return {
|
|
20
|
+
stdout: stdout.toString(),
|
|
21
|
+
stderr: '',
|
|
22
|
+
exitCode: 0,
|
|
23
|
+
};
|
|
24
|
+
} catch (error: any) {
|
|
25
|
+
return {
|
|
26
|
+
stdout: error.stdout ? error.stdout.toString() : '',
|
|
27
|
+
stderr: error.stderr ? error.stderr.toString() : '',
|
|
28
|
+
exitCode: error.status !== undefined ? error.status : 1,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function sanitizeStdout(stdout: string): string {
|
|
34
|
+
return stdout
|
|
35
|
+
.replace(/(Created|Updated):\s+.*/g, (_, label) => `${label}: [DATE]`)
|
|
36
|
+
.replace(/\b[A-Za-z]{3}\s+\d{1,2}\b/g, '[DATE]')
|
|
37
|
+
.replace(/\d{4}-\d{2}-\d{2}/g, '[DATE]')
|
|
38
|
+
.replace(/\d{1,2}:\d{2}\s+(AM|PM)/g, '[TIME]');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
describe('WorkLog CLI Productivity Commands', () => {
|
|
42
|
+
const tempProdDir = path.join(process.cwd(), 'src', 'tests', 'temp-prod-test');
|
|
43
|
+
|
|
44
|
+
beforeAll(() => {
|
|
45
|
+
if (fs.existsSync(tempProdDir)) {
|
|
46
|
+
fs.rmSync(tempProdDir, { recursive: true, force: true });
|
|
47
|
+
}
|
|
48
|
+
fs.mkdirSync(tempProdDir, { recursive: true });
|
|
49
|
+
runCLI(['init'], tempProdDir);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
afterAll(() => {
|
|
53
|
+
if (fs.existsSync(tempProdDir)) {
|
|
54
|
+
fs.rmSync(tempProdDir, { recursive: true, force: true });
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should run all commands successfully on empty repository', () => {
|
|
59
|
+
expect(runCLI(['today'], tempProdDir).stdout.trim()).toBe('No work units logged today.');
|
|
60
|
+
expect(runCLI(['week'], tempProdDir).stdout.trim()).toBe(
|
|
61
|
+
'No work units logged in the past 7 days.',
|
|
62
|
+
);
|
|
63
|
+
expect(runCLI(['dashboard'], tempProdDir).exitCode).toBe(0);
|
|
64
|
+
expect(runCLI(['continue'], tempProdDir).stdout.trim()).toBe('No unfinished work units found.');
|
|
65
|
+
expect(runCLI(['stats'], tempProdDir).exitCode).toBe(0);
|
|
66
|
+
expect(runCLI(['export'], tempProdDir).stdout.trim()).toBe('No work units found.');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should support Unicode characters and long text', () => {
|
|
70
|
+
const unicodeTitle = 'Unicode test ☕ 汉字';
|
|
71
|
+
const res = runCLI(
|
|
72
|
+
['add', '--title', `"${unicodeTitle}"`, '--type', 'idea', '--status', 'planned'],
|
|
73
|
+
tempProdDir,
|
|
74
|
+
);
|
|
75
|
+
expect(res.exitCode).toBe(0);
|
|
76
|
+
|
|
77
|
+
const showRes = runCLI(['show', '1', '--json'], tempProdDir);
|
|
78
|
+
const unit = JSON.parse(showRes.stdout);
|
|
79
|
+
expect(unit.title).toBe(unicodeTitle);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should perform advanced search by tags and status', () => {
|
|
83
|
+
runCLI(
|
|
84
|
+
[
|
|
85
|
+
'add',
|
|
86
|
+
'--title',
|
|
87
|
+
'"Tags task"',
|
|
88
|
+
'--type',
|
|
89
|
+
'bug',
|
|
90
|
+
'--status',
|
|
91
|
+
'blocked',
|
|
92
|
+
'--tags',
|
|
93
|
+
'frontend,issue-9',
|
|
94
|
+
'--module',
|
|
95
|
+
'billing',
|
|
96
|
+
],
|
|
97
|
+
tempProdDir,
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
// Search tag
|
|
101
|
+
const searchTag = runCLI(['search', '--tag', 'frontend', '--json'], tempProdDir);
|
|
102
|
+
const results = JSON.parse(searchTag.stdout);
|
|
103
|
+
expect(results.length).toBe(1);
|
|
104
|
+
expect(results[0].title).toBe('Tags task');
|
|
105
|
+
|
|
106
|
+
// Combine FTS text + tag filter
|
|
107
|
+
const searchCombined = runCLI(['search', 'Tags', '--status', 'blocked', '--json'], tempProdDir);
|
|
108
|
+
const results2 = JSON.parse(searchCombined.stdout);
|
|
109
|
+
expect(results2.length).toBe(1);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('should run continue command and resume last unfinished unit', () => {
|
|
113
|
+
// We have unit #1 (Planned) and unit #2 (Blocked)
|
|
114
|
+
// Run continue (will choose unit 2 since it was updated last)
|
|
115
|
+
const res = runCLI(['continue', '--json'], tempProdDir);
|
|
116
|
+
expect(res.exitCode).toBe(0);
|
|
117
|
+
const data = JSON.parse(res.stdout);
|
|
118
|
+
expect(data.id).toBe(2);
|
|
119
|
+
expect(data.status).toBe('In Progress');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('should display dashboard (snapshot)', () => {
|
|
123
|
+
const res = runCLI(['dashboard'], tempProdDir);
|
|
124
|
+
expect(res.exitCode).toBe(0);
|
|
125
|
+
expect(sanitizeStdout(res.stdout)).toMatchSnapshot();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('should display stats summary (snapshot)', () => {
|
|
129
|
+
const res = runCLI(['stats'], tempProdDir);
|
|
130
|
+
expect(res.exitCode).toBe(0);
|
|
131
|
+
expect(sanitizeStdout(res.stdout)).toMatchSnapshot();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('should export all to markdown and JSON file', () => {
|
|
135
|
+
const mdPath = path.join(tempProdDir, 'export.md');
|
|
136
|
+
const resMd = runCLI(
|
|
137
|
+
['export', '--format', 'markdown', '--output', `"${mdPath}"`],
|
|
138
|
+
tempProdDir,
|
|
139
|
+
);
|
|
140
|
+
expect(resMd.exitCode).toBe(0);
|
|
141
|
+
expect(fs.existsSync(mdPath)).toBe(true);
|
|
142
|
+
expect(fs.readFileSync(mdPath, 'utf8')).toContain('# WorkLog Export');
|
|
143
|
+
|
|
144
|
+
const singleJSON = runCLI(['export', '--id', '1', '--format', 'json'], tempProdDir);
|
|
145
|
+
expect(singleJSON.exitCode).toBe(0);
|
|
146
|
+
const data = JSON.parse(singleJSON.stdout);
|
|
147
|
+
expect(data.id).toBe(1);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('should throw exit code 1 on invalid export path', () => {
|
|
151
|
+
const res = runCLI(
|
|
152
|
+
['export', '--format', 'markdown', '--output', '"/non/existent/path/export.md"'],
|
|
153
|
+
tempProdDir,
|
|
154
|
+
);
|
|
155
|
+
expect(res.exitCode).toBe(1);
|
|
156
|
+
expect(res.stderr).toContain('Failed to write to file');
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('should handle database corruption/unexpected failures gracefully with exit code 2', () => {
|
|
160
|
+
const dbPath = path.join(tempProdDir, '.worklog', 'worklog.db');
|
|
161
|
+
fs.writeFileSync(dbPath, 'completely corrupted database text', 'utf8');
|
|
162
|
+
|
|
163
|
+
const res = runCLI(['list'], tempProdDir);
|
|
164
|
+
expect(res.exitCode).toBe(2);
|
|
165
|
+
expect(res.stderr).toContain('Unexpected Error');
|
|
166
|
+
});
|
|
167
|
+
});
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { execSync } from 'child_process';
|
|
5
|
+
|
|
6
|
+
interface CLIResult {
|
|
7
|
+
stdout: string;
|
|
8
|
+
stderr: string;
|
|
9
|
+
exitCode: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function runCLI(args: string[], cwd: string): CLIResult {
|
|
13
|
+
const cliPath = path.resolve(process.cwd(), 'dist', 'index.js');
|
|
14
|
+
try {
|
|
15
|
+
const stdout = execSync(`node ${cliPath} ${args.join(' ')}`, {
|
|
16
|
+
cwd,
|
|
17
|
+
stdio: 'pipe',
|
|
18
|
+
});
|
|
19
|
+
return {
|
|
20
|
+
stdout: stdout.toString(),
|
|
21
|
+
stderr: '',
|
|
22
|
+
exitCode: 0,
|
|
23
|
+
};
|
|
24
|
+
} catch (error: any) {
|
|
25
|
+
return {
|
|
26
|
+
stdout: error.stdout ? error.stdout.toString() : '',
|
|
27
|
+
stderr: error.stderr ? error.stderr.toString() : '',
|
|
28
|
+
exitCode: error.status !== undefined ? error.status : 1,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function sanitizeStdout(stdout: string): string {
|
|
34
|
+
return stdout
|
|
35
|
+
.replace(/(Created|Updated):\s+.*/g, (_, label) => `${label}: [DATE]`)
|
|
36
|
+
.replace(/\b[A-Za-z]{3}\s+\d{1,2}\b/g, '[DATE]');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe('WorkLog CLI Integration', () => {
|
|
40
|
+
const tempCLIDir = path.join(process.cwd(), 'src', 'tests', 'temp-cli-test');
|
|
41
|
+
|
|
42
|
+
beforeAll(() => {
|
|
43
|
+
if (fs.existsSync(tempCLIDir)) {
|
|
44
|
+
fs.rmSync(tempCLIDir, { recursive: true, force: true });
|
|
45
|
+
}
|
|
46
|
+
fs.mkdirSync(tempCLIDir, { recursive: true });
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
afterAll(() => {
|
|
50
|
+
if (fs.existsSync(tempCLIDir)) {
|
|
51
|
+
fs.rmSync(tempCLIDir, { recursive: true, force: true });
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should fail when running list outside a initialized repository', () => {
|
|
56
|
+
const res = runCLI(['list'], tempCLIDir);
|
|
57
|
+
expect(res.exitCode).toBe(1);
|
|
58
|
+
expect(res.stderr).toContain('Not a WorkLog repository');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should initialize a empty repository successfully', () => {
|
|
62
|
+
const res = runCLI(['init'], tempCLIDir);
|
|
63
|
+
expect(res.exitCode).toBe(0);
|
|
64
|
+
expect(res.stdout).toContain('Initialized empty WorkLog repository');
|
|
65
|
+
|
|
66
|
+
// Check files are created
|
|
67
|
+
expect(fs.existsSync(path.join(tempCLIDir, '.worklog'))).toBe(true);
|
|
68
|
+
expect(fs.existsSync(path.join(tempCLIDir, '.worklog', 'config.json'))).toBe(true);
|
|
69
|
+
expect(fs.existsSync(path.join(tempCLIDir, '.worklog', 'worklog.db'))).toBe(true);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should list empty results when database is empty', () => {
|
|
73
|
+
const res = runCLI(['list'], tempCLIDir);
|
|
74
|
+
expect(res.exitCode).toBe(0);
|
|
75
|
+
expect(res.stdout.trim()).toBe('No work units found.');
|
|
76
|
+
|
|
77
|
+
const resJson = runCLI(['list', '--json'], tempCLIDir);
|
|
78
|
+
expect(resJson.exitCode).toBe(0);
|
|
79
|
+
expect(JSON.parse(resJson.stdout)).toEqual([]);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should create a work unit with flags', () => {
|
|
83
|
+
const res = runCLI(
|
|
84
|
+
[
|
|
85
|
+
'add',
|
|
86
|
+
'--title',
|
|
87
|
+
'"First Task"',
|
|
88
|
+
'--type',
|
|
89
|
+
'feature',
|
|
90
|
+
'--status',
|
|
91
|
+
'planned',
|
|
92
|
+
'--module',
|
|
93
|
+
'auth',
|
|
94
|
+
'--tags',
|
|
95
|
+
'login,security',
|
|
96
|
+
],
|
|
97
|
+
tempCLIDir,
|
|
98
|
+
);
|
|
99
|
+
expect(res.exitCode).toBe(0);
|
|
100
|
+
expect(res.stdout).toContain('Created work unit #1: First Task');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('should support json flag on add', () => {
|
|
104
|
+
const res = runCLI(
|
|
105
|
+
['add', '--title', '"Second Task"', '--type', 'bug', '--status', 'in-progress', '--json'],
|
|
106
|
+
tempCLIDir,
|
|
107
|
+
);
|
|
108
|
+
expect(res.exitCode).toBe(0);
|
|
109
|
+
const data = JSON.parse(res.stdout);
|
|
110
|
+
expect(data.id).toBe(2);
|
|
111
|
+
expect(data.title).toBe('Second Task');
|
|
112
|
+
expect(data.type).toBe('Bug');
|
|
113
|
+
expect(data.status).toBe('In Progress');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('should output validation errors cleanly and exit with 1', () => {
|
|
117
|
+
// Title too long
|
|
118
|
+
const longTitle = 'a'.repeat(250);
|
|
119
|
+
const res = runCLI(['add', '--title', `"${longTitle}"`], tempCLIDir);
|
|
120
|
+
expect(res.exitCode).toBe(1);
|
|
121
|
+
expect(res.stderr).toContain('Title cannot exceed 200 characters');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('should list work units in a beautiful table format (snapshot)', () => {
|
|
125
|
+
const res = runCLI(['list'], tempCLIDir);
|
|
126
|
+
expect(res.exitCode).toBe(0);
|
|
127
|
+
// Snapshot checking of the table representation
|
|
128
|
+
expect(sanitizeStdout(res.stdout)).toMatchSnapshot();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('should list work units in JSON format', () => {
|
|
132
|
+
const res = runCLI(['list', '--json'], tempCLIDir);
|
|
133
|
+
expect(res.exitCode).toBe(0);
|
|
134
|
+
const data = JSON.parse(res.stdout);
|
|
135
|
+
expect(data.length).toBe(2);
|
|
136
|
+
expect(data[0].id).toBe(2);
|
|
137
|
+
expect(data[1].id).toBe(1);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('should show card details for a specific unit (snapshot)', () => {
|
|
141
|
+
const res = runCLI(['show', '1'], tempCLIDir);
|
|
142
|
+
expect(res.exitCode).toBe(0);
|
|
143
|
+
expect(sanitizeStdout(res.stdout)).toMatchSnapshot();
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('should show JSON details for a specific unit', () => {
|
|
147
|
+
const res = runCLI(['show', '2', '--json'], tempCLIDir);
|
|
148
|
+
expect(res.exitCode).toBe(0);
|
|
149
|
+
const data = JSON.parse(res.stdout);
|
|
150
|
+
expect(data.id).toBe(2);
|
|
151
|
+
expect(data.title).toBe('Second Task');
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('should show error and exit with 1 if unit does not exist', () => {
|
|
155
|
+
const res = runCLI(['show', '999'], tempCLIDir);
|
|
156
|
+
expect(res.exitCode).toBe(1);
|
|
157
|
+
expect(res.stderr).toContain('Work unit #999 not found');
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('should handle unknown command routing', () => {
|
|
161
|
+
const res = runCLI(['hello-unknown'], tempCLIDir);
|
|
162
|
+
expect(res.exitCode).toBe(1);
|
|
163
|
+
expect(res.stderr).toContain('Unknown command: hello-unknown');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('should cancel add if title is missing in non-TTY mode', () => {
|
|
167
|
+
// Since title is missing and stdin is closed in runCLI subprocess, it should exit with cancel code 1
|
|
168
|
+
const res = runCLI(['add'], tempCLIDir);
|
|
169
|
+
expect(res.exitCode).toBe(1);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { loadConfig, saveConfig, initConfig } from '../../core/config.js';
|
|
5
|
+
import { ConfigError } from '../../errors/index.js';
|
|
6
|
+
|
|
7
|
+
describe('config', () => {
|
|
8
|
+
const tempTestDir = path.join(process.cwd(), 'src', 'tests', 'temp-config-test');
|
|
9
|
+
|
|
10
|
+
beforeAll(() => {
|
|
11
|
+
if (fs.existsSync(tempTestDir)) {
|
|
12
|
+
fs.rmSync(tempTestDir, { recursive: true, force: true });
|
|
13
|
+
}
|
|
14
|
+
fs.mkdirSync(tempTestDir, { recursive: true });
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
afterAll(() => {
|
|
18
|
+
if (fs.existsSync(tempTestDir)) {
|
|
19
|
+
fs.rmSync(tempTestDir, { recursive: true, force: true });
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should initialize config with default values', () => {
|
|
24
|
+
const config = initConfig(tempTestDir, 'test-project');
|
|
25
|
+
expect(config.projectName).toBe('test-project');
|
|
26
|
+
expect(config.schemaVersion).toBe(1);
|
|
27
|
+
expect(config.createdAt).toBeDefined();
|
|
28
|
+
|
|
29
|
+
// Verify it wrote the file
|
|
30
|
+
const fileExists = fs.existsSync(path.join(tempTestDir, '.worklog', 'config.json'));
|
|
31
|
+
expect(fileExists).toBe(true);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should load config that has been saved', () => {
|
|
35
|
+
const loaded = loadConfig(tempTestDir);
|
|
36
|
+
expect(loaded.projectName).toBe('test-project');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should throw ConfigError if config is missing', () => {
|
|
40
|
+
const emptyDir = path.join(tempTestDir, 'empty');
|
|
41
|
+
fs.mkdirSync(emptyDir, { recursive: true });
|
|
42
|
+
expect(() => loadConfig(emptyDir)).toThrow(ConfigError);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should throw ConfigError if config format is invalid', () => {
|
|
46
|
+
const invalidDir = path.join(tempTestDir, 'invalid');
|
|
47
|
+
const dotWorklog = path.join(invalidDir, '.worklog');
|
|
48
|
+
fs.mkdirSync(dotWorklog, { recursive: true });
|
|
49
|
+
fs.writeFileSync(path.join(dotWorklog, 'config.json'), '{"invalid": "json"}', 'utf8');
|
|
50
|
+
|
|
51
|
+
expect(() => loadConfig(invalidDir)).toThrow(ConfigError);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { findWorkspaceRoot, getWorkspaceRoot } from '../../core/discovery.js';
|
|
5
|
+
import { NotInitializedError } from '../../errors/index.js';
|
|
6
|
+
|
|
7
|
+
describe('discovery', () => {
|
|
8
|
+
const tempTestDir = path.join(process.cwd(), 'src', 'tests', 'temp-discovery-test');
|
|
9
|
+
|
|
10
|
+
beforeAll(() => {
|
|
11
|
+
if (fs.existsSync(tempTestDir)) {
|
|
12
|
+
fs.rmSync(tempTestDir, { recursive: true, force: true });
|
|
13
|
+
}
|
|
14
|
+
fs.mkdirSync(tempTestDir, { recursive: true });
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
afterAll(() => {
|
|
18
|
+
if (fs.existsSync(tempTestDir)) {
|
|
19
|
+
fs.rmSync(tempTestDir, { recursive: true, force: true });
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should return null when .worklog is not present in parent chain', () => {
|
|
24
|
+
const subDir = path.join(tempTestDir, 'a', 'b', 'c');
|
|
25
|
+
fs.mkdirSync(subDir, { recursive: true });
|
|
26
|
+
expect(findWorkspaceRoot(subDir)).toBeNull();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should find the .worklog root when it exists in a parent directory', () => {
|
|
30
|
+
const worklogRoot = path.join(tempTestDir, 'project-root');
|
|
31
|
+
const dotWorklog = path.join(worklogRoot, '.worklog');
|
|
32
|
+
const subDir = path.join(worklogRoot, 'src', 'core');
|
|
33
|
+
|
|
34
|
+
fs.mkdirSync(dotWorklog, { recursive: true });
|
|
35
|
+
fs.mkdirSync(subDir, { recursive: true });
|
|
36
|
+
|
|
37
|
+
const discovered = findWorkspaceRoot(subDir);
|
|
38
|
+
expect(discovered).toBe(worklogRoot);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should throw NotInitializedError if not initialized', () => {
|
|
42
|
+
const nonExistentDir = path.join(tempTestDir, 'non-existent');
|
|
43
|
+
fs.mkdirSync(nonExistentDir, { recursive: true });
|
|
44
|
+
expect(() => getWorkspaceRoot(nonExistentDir)).toThrow(NotInitializedError);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import Database from 'better-sqlite3';
|
|
5
|
+
import { runMigrations } from '../../database/migrations.js';
|
|
6
|
+
|
|
7
|
+
describe('migrations', () => {
|
|
8
|
+
const tempTestDir = path.join(process.cwd(), 'src', 'tests', 'temp-migrations-test');
|
|
9
|
+
let db: Database.Database;
|
|
10
|
+
|
|
11
|
+
beforeAll(() => {
|
|
12
|
+
if (fs.existsSync(tempTestDir)) {
|
|
13
|
+
fs.rmSync(tempTestDir, { recursive: true, force: true });
|
|
14
|
+
}
|
|
15
|
+
fs.mkdirSync(tempTestDir, { recursive: true });
|
|
16
|
+
|
|
17
|
+
const dbPath = path.join(tempTestDir, 'test.db');
|
|
18
|
+
db = new Database(dbPath);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
afterAll(() => {
|
|
22
|
+
if (db) {
|
|
23
|
+
db.close();
|
|
24
|
+
}
|
|
25
|
+
if (fs.existsSync(tempTestDir)) {
|
|
26
|
+
fs.rmSync(tempTestDir, { recursive: true, force: true });
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should run migrations successfully and create tables', () => {
|
|
31
|
+
runMigrations(db);
|
|
32
|
+
|
|
33
|
+
// Verify migration tracking table has records for both files
|
|
34
|
+
const migrations = db.prepare('SELECT version FROM schema_migrations ORDER BY version ASC').all();
|
|
35
|
+
expect(migrations.length).toBe(2);
|
|
36
|
+
expect(migrations[0]).toEqual({ version: '001_initial.sql' });
|
|
37
|
+
expect(migrations[1]).toEqual({ version: '002_relational_tables.sql' });
|
|
38
|
+
|
|
39
|
+
// Verify work_units table exists and we can insert a row
|
|
40
|
+
const insertStmt = db.prepare(`
|
|
41
|
+
INSERT INTO work_units (title, type, status, created_at, updated_at)
|
|
42
|
+
VALUES (?, ?, ?, ?, ?)
|
|
43
|
+
`);
|
|
44
|
+
const info = insertStmt.run(
|
|
45
|
+
'Test Work Unit',
|
|
46
|
+
'Feature',
|
|
47
|
+
'In Progress',
|
|
48
|
+
new Date().toISOString(),
|
|
49
|
+
new Date().toISOString(),
|
|
50
|
+
);
|
|
51
|
+
expect(info.changes).toBe(1);
|
|
52
|
+
const workUnitId = info.lastInsertRowid;
|
|
53
|
+
|
|
54
|
+
// Verify work_unit_tags table exists and works
|
|
55
|
+
const tagInsert = db.prepare('INSERT INTO work_unit_tags (work_unit_id, tag) VALUES (?, ?)');
|
|
56
|
+
const tagInfo = tagInsert.run(workUnitId, 'cli');
|
|
57
|
+
expect(tagInfo.changes).toBe(1);
|
|
58
|
+
|
|
59
|
+
// Verify work_unit_relations table exists and works
|
|
60
|
+
const relInsert = db.prepare('INSERT INTO work_unit_relations (work_unit_id, related_work_unit_id) VALUES (?, ?)');
|
|
61
|
+
const relInfo = relInsert.run(workUnitId, workUnitId); // self-referencing relationship for testing
|
|
62
|
+
expect(relInfo.changes).toBe(1);
|
|
63
|
+
|
|
64
|
+
// Verify FTS table matches and can be queried
|
|
65
|
+
const ftsResults = db.prepare(`
|
|
66
|
+
SELECT rowid, title FROM work_units_fts WHERE work_units_fts MATCH 'Test'
|
|
67
|
+
`).all();
|
|
68
|
+
expect(ftsResults.length).toBe(1);
|
|
69
|
+
expect((ftsResults[0] as any).title).toBe('Test Work Unit');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should not run migrations twice if already applied', () => {
|
|
73
|
+
expect(() => runMigrations(db)).not.toThrow();
|
|
74
|
+
});
|
|
75
|
+
});
|