@towles/tool 0.0.18 → 0.0.41
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/LICENSE +21 -0
- package/LICENSE.md +9 -10
- package/README.md +121 -78
- package/bin/run.ts +5 -0
- package/package.json +63 -53
- package/patches/prompts.patch +34 -0
- package/src/commands/base.ts +42 -0
- package/src/commands/config.test.ts +15 -0
- package/src/commands/config.ts +43 -0
- package/src/commands/doctor.ts +133 -0
- package/src/commands/gh/branch-clean.ts +110 -0
- package/src/commands/gh/branch.test.ts +124 -0
- package/src/commands/gh/branch.ts +132 -0
- package/src/commands/gh/pr.ts +168 -0
- package/src/commands/index.ts +55 -0
- package/src/commands/install.ts +148 -0
- package/src/commands/journal/daily-notes.ts +66 -0
- package/src/commands/journal/meeting.ts +83 -0
- package/src/commands/journal/note.ts +83 -0
- package/src/commands/journal/utils.ts +399 -0
- package/src/commands/observe/graph.test.ts +89 -0
- package/src/commands/observe/graph.ts +1640 -0
- package/src/commands/observe/report.ts +166 -0
- package/src/commands/observe/session.ts +385 -0
- package/src/commands/observe/setup.ts +180 -0
- package/src/commands/observe/status.ts +146 -0
- package/src/commands/ralph/lib/execution.ts +302 -0
- package/src/commands/ralph/lib/formatter.ts +298 -0
- package/src/commands/ralph/lib/index.ts +4 -0
- package/src/commands/ralph/lib/marker.ts +108 -0
- package/src/commands/ralph/lib/state.ts +191 -0
- package/src/commands/ralph/marker/create.ts +23 -0
- package/src/commands/ralph/plan.ts +73 -0
- package/src/commands/ralph/progress.ts +44 -0
- package/src/commands/ralph/ralph.test.ts +673 -0
- package/src/commands/ralph/run.ts +408 -0
- package/src/commands/ralph/task/add.ts +105 -0
- package/src/commands/ralph/task/done.ts +73 -0
- package/src/commands/ralph/task/list.test.ts +48 -0
- package/src/commands/ralph/task/list.ts +110 -0
- package/src/commands/ralph/task/remove.ts +62 -0
- package/src/config/context.ts +7 -0
- package/src/config/settings.ts +155 -0
- package/src/constants.ts +3 -0
- package/src/types/journal.ts +16 -0
- package/src/utils/anthropic/types.ts +158 -0
- package/src/utils/date-utils.test.ts +96 -0
- package/src/utils/date-utils.ts +54 -0
- package/src/utils/exec.ts +8 -0
- package/src/utils/git/gh-cli-wrapper.test.ts +14 -0
- package/src/utils/git/gh-cli-wrapper.ts +54 -0
- package/src/utils/git/git-wrapper.test.ts +26 -0
- package/src/utils/git/git-wrapper.ts +15 -0
- package/src/utils/git/git.ts +25 -0
- package/src/utils/render.test.ts +71 -0
- package/src/utils/render.ts +34 -0
- package/dist/index.d.mts +0 -1
- package/dist/index.mjs +0 -794
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
import { exec } from "node:child_process";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { promisify } from "node:util";
|
|
5
|
+
import consola from "consola";
|
|
6
|
+
import { colors } from "consola/utils";
|
|
7
|
+
import { DateTime } from "luxon";
|
|
8
|
+
import { formatDate, getMondayOfWeek, getWeekInfo } from "../../utils/date-utils.js";
|
|
9
|
+
import type { JournalSettings } from "../../config/settings.js";
|
|
10
|
+
import { JOURNAL_TYPES } from "../../types/journal.js";
|
|
11
|
+
import type { JournalType } from "../../types/journal.js";
|
|
12
|
+
|
|
13
|
+
// Default template file names
|
|
14
|
+
const TEMPLATE_FILES = {
|
|
15
|
+
dailyNotes: "daily-notes.md",
|
|
16
|
+
meeting: "meeting.md",
|
|
17
|
+
note: "note.md",
|
|
18
|
+
} as const;
|
|
19
|
+
|
|
20
|
+
const execAsync = promisify(exec);
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Create journal directory if it doesn't exist
|
|
24
|
+
*/
|
|
25
|
+
export function ensureDirectoryExists(folderPath: string): void {
|
|
26
|
+
if (!existsSync(folderPath)) {
|
|
27
|
+
consola.info(`Creating journal directory: ${colors.cyan(folderPath)}`);
|
|
28
|
+
mkdirSync(folderPath, { recursive: true });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Load template from external file or return null if not found
|
|
34
|
+
*/
|
|
35
|
+
export function loadTemplate(templateDir: string, templateFile: string): string | null {
|
|
36
|
+
const templatePath = path.join(templateDir, templateFile);
|
|
37
|
+
if (existsSync(templatePath)) {
|
|
38
|
+
return readFileSync(templatePath, "utf8");
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Get default template content for initial setup
|
|
45
|
+
*/
|
|
46
|
+
function getDefaultDailyNotesTemplate(): string {
|
|
47
|
+
return `# Journal for Week {monday:yyyy-MM-dd}
|
|
48
|
+
|
|
49
|
+
## {monday:yyyy-MM-dd} Monday
|
|
50
|
+
|
|
51
|
+
## {tuesday:yyyy-MM-dd} Tuesday
|
|
52
|
+
|
|
53
|
+
## {wednesday:yyyy-MM-dd} Wednesday
|
|
54
|
+
|
|
55
|
+
## {thursday:yyyy-MM-dd} Thursday
|
|
56
|
+
|
|
57
|
+
## {friday:yyyy-MM-dd} Friday
|
|
58
|
+
`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function getDefaultMeetingTemplate(): string {
|
|
62
|
+
return `# Meeting: {title}
|
|
63
|
+
|
|
64
|
+
**Date:** {date}
|
|
65
|
+
**Time:** {time}
|
|
66
|
+
**Attendees:**
|
|
67
|
+
|
|
68
|
+
## Agenda
|
|
69
|
+
|
|
70
|
+
-
|
|
71
|
+
|
|
72
|
+
## Notes
|
|
73
|
+
|
|
74
|
+
## Action Items
|
|
75
|
+
|
|
76
|
+
- [ ]
|
|
77
|
+
|
|
78
|
+
## Follow-up
|
|
79
|
+
`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function getDefaultNoteTemplate(): string {
|
|
83
|
+
return `# {title}
|
|
84
|
+
|
|
85
|
+
**Created:** {date} {time}
|
|
86
|
+
|
|
87
|
+
## Summary
|
|
88
|
+
|
|
89
|
+
## Details
|
|
90
|
+
|
|
91
|
+
## References
|
|
92
|
+
`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Initialize template directory with default templates (first run)
|
|
97
|
+
*/
|
|
98
|
+
export function ensureTemplatesExist(templateDir: string): void {
|
|
99
|
+
ensureDirectoryExists(templateDir);
|
|
100
|
+
|
|
101
|
+
const templates = [
|
|
102
|
+
{ file: TEMPLATE_FILES.dailyNotes, content: getDefaultDailyNotesTemplate() },
|
|
103
|
+
{ file: TEMPLATE_FILES.meeting, content: getDefaultMeetingTemplate() },
|
|
104
|
+
{ file: TEMPLATE_FILES.note, content: getDefaultNoteTemplate() },
|
|
105
|
+
];
|
|
106
|
+
|
|
107
|
+
for (const { file, content } of templates) {
|
|
108
|
+
const templatePath = path.join(templateDir, file);
|
|
109
|
+
if (!existsSync(templatePath)) {
|
|
110
|
+
writeFileSync(templatePath, content, "utf8");
|
|
111
|
+
consola.info(`Created default template: ${colors.cyan(templatePath)}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Render template with variables
|
|
118
|
+
*/
|
|
119
|
+
function renderTemplate(template: string, vars: Record<string, string>): string {
|
|
120
|
+
return template.replace(/\{([^}]+)\}/g, (match, key) => {
|
|
121
|
+
return vars[key] ?? match;
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Create initial journal content with date header
|
|
127
|
+
*/
|
|
128
|
+
export function createJournalContent({
|
|
129
|
+
mondayDate,
|
|
130
|
+
templateDir,
|
|
131
|
+
}: {
|
|
132
|
+
mondayDate: Date;
|
|
133
|
+
templateDir?: string;
|
|
134
|
+
}): string {
|
|
135
|
+
const weekInfo = getWeekInfo(mondayDate);
|
|
136
|
+
|
|
137
|
+
// Try external template first
|
|
138
|
+
if (templateDir) {
|
|
139
|
+
const externalTemplate = loadTemplate(templateDir, TEMPLATE_FILES.dailyNotes);
|
|
140
|
+
if (externalTemplate) {
|
|
141
|
+
return renderTemplate(externalTemplate, {
|
|
142
|
+
"monday:yyyy-MM-dd": formatDate(weekInfo.mondayDate),
|
|
143
|
+
"tuesday:yyyy-MM-dd": formatDate(weekInfo.tuesdayDate),
|
|
144
|
+
"wednesday:yyyy-MM-dd": formatDate(weekInfo.wednesdayDate),
|
|
145
|
+
"thursday:yyyy-MM-dd": formatDate(weekInfo.thursdayDate),
|
|
146
|
+
"friday:yyyy-MM-dd": formatDate(weekInfo.fridayDate),
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Fallback to hardcoded template
|
|
152
|
+
const content = [`# Journal for Week ${formatDate(mondayDate)}`];
|
|
153
|
+
content.push(``);
|
|
154
|
+
content.push(`## ${formatDate(weekInfo.mondayDate)} Monday`);
|
|
155
|
+
content.push(``);
|
|
156
|
+
content.push(`## ${formatDate(weekInfo.tuesdayDate)} Tuesday`);
|
|
157
|
+
content.push(``);
|
|
158
|
+
content.push(`## ${formatDate(weekInfo.wednesdayDate)} Wednesday`);
|
|
159
|
+
content.push(``);
|
|
160
|
+
content.push(`## ${formatDate(weekInfo.thursdayDate)} Thursday`);
|
|
161
|
+
content.push(``);
|
|
162
|
+
content.push(`## ${formatDate(weekInfo.fridayDate)} Friday`);
|
|
163
|
+
content.push(``);
|
|
164
|
+
|
|
165
|
+
return content.join("\n");
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Create meeting template content
|
|
170
|
+
*/
|
|
171
|
+
export function createMeetingContent({
|
|
172
|
+
title,
|
|
173
|
+
date,
|
|
174
|
+
templateDir,
|
|
175
|
+
}: {
|
|
176
|
+
title?: string;
|
|
177
|
+
date: Date;
|
|
178
|
+
templateDir?: string;
|
|
179
|
+
}): string {
|
|
180
|
+
const dateStr = formatDate(date);
|
|
181
|
+
const timeStr = date.toLocaleTimeString("en-US", {
|
|
182
|
+
hour12: false,
|
|
183
|
+
hour: "2-digit",
|
|
184
|
+
minute: "2-digit",
|
|
185
|
+
});
|
|
186
|
+
const meetingTitle = title || "Meeting";
|
|
187
|
+
|
|
188
|
+
// Try external template first
|
|
189
|
+
if (templateDir) {
|
|
190
|
+
const externalTemplate = loadTemplate(templateDir, TEMPLATE_FILES.meeting);
|
|
191
|
+
if (externalTemplate) {
|
|
192
|
+
return renderTemplate(externalTemplate, {
|
|
193
|
+
title: meetingTitle,
|
|
194
|
+
date: dateStr,
|
|
195
|
+
time: timeStr,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Fallback to hardcoded template
|
|
201
|
+
const content = [`# Meeting: ${meetingTitle}`];
|
|
202
|
+
content.push(``);
|
|
203
|
+
content.push(`**Date:** ${dateStr}`);
|
|
204
|
+
content.push(`**Time:** ${timeStr}`);
|
|
205
|
+
content.push(`**Attendees:** `);
|
|
206
|
+
content.push(``);
|
|
207
|
+
content.push(`## Agenda`);
|
|
208
|
+
content.push(``);
|
|
209
|
+
content.push(`- `);
|
|
210
|
+
content.push(``);
|
|
211
|
+
content.push(`## Notes`);
|
|
212
|
+
content.push(``);
|
|
213
|
+
content.push(`## Action Items`);
|
|
214
|
+
content.push(``);
|
|
215
|
+
content.push(`- [ ] `);
|
|
216
|
+
content.push(``);
|
|
217
|
+
content.push(`## Follow-up`);
|
|
218
|
+
content.push(``);
|
|
219
|
+
|
|
220
|
+
return content.join("\n");
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Create note template content
|
|
225
|
+
*/
|
|
226
|
+
export function createNoteContent({
|
|
227
|
+
title,
|
|
228
|
+
date,
|
|
229
|
+
templateDir,
|
|
230
|
+
}: {
|
|
231
|
+
title?: string;
|
|
232
|
+
date: Date;
|
|
233
|
+
templateDir?: string;
|
|
234
|
+
}): string {
|
|
235
|
+
const dateStr = formatDate(date);
|
|
236
|
+
const timeStr = date.toLocaleTimeString("en-US", {
|
|
237
|
+
hour12: false,
|
|
238
|
+
hour: "2-digit",
|
|
239
|
+
minute: "2-digit",
|
|
240
|
+
});
|
|
241
|
+
const noteTitle = title || "Note";
|
|
242
|
+
|
|
243
|
+
// Try external template first
|
|
244
|
+
if (templateDir) {
|
|
245
|
+
const externalTemplate = loadTemplate(templateDir, TEMPLATE_FILES.note);
|
|
246
|
+
if (externalTemplate) {
|
|
247
|
+
return renderTemplate(externalTemplate, {
|
|
248
|
+
title: noteTitle,
|
|
249
|
+
date: dateStr,
|
|
250
|
+
time: timeStr,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Fallback to hardcoded template
|
|
256
|
+
const content = [`# ${noteTitle}`];
|
|
257
|
+
content.push(``);
|
|
258
|
+
content.push(`**Created:** ${dateStr} ${timeStr}`);
|
|
259
|
+
content.push(``);
|
|
260
|
+
content.push(`## Summary`);
|
|
261
|
+
content.push(``);
|
|
262
|
+
content.push(`## Details`);
|
|
263
|
+
content.push(``);
|
|
264
|
+
content.push(`## References`);
|
|
265
|
+
content.push(``);
|
|
266
|
+
|
|
267
|
+
return content.join("\n");
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Open file in default editor with folder context
|
|
272
|
+
*/
|
|
273
|
+
export async function openInEditor({
|
|
274
|
+
editor,
|
|
275
|
+
filePath,
|
|
276
|
+
folderPath,
|
|
277
|
+
}: {
|
|
278
|
+
editor: string;
|
|
279
|
+
filePath: string;
|
|
280
|
+
folderPath?: string;
|
|
281
|
+
}): Promise<void> {
|
|
282
|
+
try {
|
|
283
|
+
if (folderPath) {
|
|
284
|
+
// Open both folder and file - this works with VS Code and similar editors
|
|
285
|
+
// the purpose is to open the folder context for better navigation
|
|
286
|
+
await execAsync(`"${editor}" "${folderPath}" "${filePath}"`);
|
|
287
|
+
} else {
|
|
288
|
+
await execAsync(`"${editor}" "${filePath}"`);
|
|
289
|
+
}
|
|
290
|
+
} catch (ex) {
|
|
291
|
+
consola.warn(
|
|
292
|
+
`Could not open in editor : '${editor}'. Modify your editor in the config: examples include 'code', 'code-insiders', etc...`,
|
|
293
|
+
ex,
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export function resolvePathTemplate(
|
|
299
|
+
template: string,
|
|
300
|
+
title: string,
|
|
301
|
+
date: Date,
|
|
302
|
+
mondayDate: Date,
|
|
303
|
+
): string {
|
|
304
|
+
const dateTime = DateTime.fromJSDate(date, { zone: "utc" });
|
|
305
|
+
|
|
306
|
+
// Replace Luxon format tokens wrapped in curly braces
|
|
307
|
+
return template.replace(/\{([^}]+)\}/g, (match, token) => {
|
|
308
|
+
try {
|
|
309
|
+
if (token === "title") {
|
|
310
|
+
return title.toLowerCase().replace(/\s+/g, "-");
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (token.startsWith("monday:")) {
|
|
314
|
+
const mondayToken = token.substring(7); // Remove 'monday:' prefix
|
|
315
|
+
const mondayDateTime = DateTime.fromJSDate(mondayDate, { zone: "utc" });
|
|
316
|
+
return mondayDateTime.toFormat(mondayToken);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const result = dateTime.toFormat(token);
|
|
320
|
+
// Check if the result contains suspicious patterns that indicate invalid tokens
|
|
321
|
+
// This is a heuristic to detect when Luxon produces garbage output for invalid tokens
|
|
322
|
+
const isLikelyInvalid =
|
|
323
|
+
token.includes("invalid") ||
|
|
324
|
+
result.length > 20 || // Very long results are likely garbage
|
|
325
|
+
(result.length > token.length * 2 && /\d{10,}/.test(result)) || // Contains very long numbers
|
|
326
|
+
result.includes("UTC");
|
|
327
|
+
|
|
328
|
+
if (isLikelyInvalid) {
|
|
329
|
+
consola.warn(`Invalid date format token: ${token}`);
|
|
330
|
+
return match;
|
|
331
|
+
}
|
|
332
|
+
return result;
|
|
333
|
+
} catch (error) {
|
|
334
|
+
consola.warn(`Invalid date format token: ${token}`);
|
|
335
|
+
return match; // Return original token if format is invalid
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
interface GenerateJournalFileResult {
|
|
341
|
+
fullPath: string;
|
|
342
|
+
mondayDate: Date;
|
|
343
|
+
currentDate: Date;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
interface GenerateJournalFileParams {
|
|
347
|
+
date: Date;
|
|
348
|
+
type: JournalType;
|
|
349
|
+
title: string;
|
|
350
|
+
journalSettings: JournalSettings;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Generate journal file info for different types using individual path templates
|
|
355
|
+
*/
|
|
356
|
+
export function generateJournalFileInfoByType({
|
|
357
|
+
journalSettings,
|
|
358
|
+
date = new Date(),
|
|
359
|
+
type,
|
|
360
|
+
title,
|
|
361
|
+
}: GenerateJournalFileParams): GenerateJournalFileResult {
|
|
362
|
+
const currentDate = new Date(date);
|
|
363
|
+
|
|
364
|
+
let templatePath: string = "";
|
|
365
|
+
let mondayDate: Date = getMondayOfWeek(currentDate);
|
|
366
|
+
|
|
367
|
+
switch (type) {
|
|
368
|
+
case JOURNAL_TYPES.DAILY_NOTES: {
|
|
369
|
+
const monday = getMondayOfWeek(currentDate);
|
|
370
|
+
templatePath = journalSettings.dailyPathTemplate;
|
|
371
|
+
mondayDate = monday;
|
|
372
|
+
break;
|
|
373
|
+
}
|
|
374
|
+
case JOURNAL_TYPES.MEETING: {
|
|
375
|
+
templatePath = journalSettings.meetingPathTemplate;
|
|
376
|
+
mondayDate = currentDate;
|
|
377
|
+
break;
|
|
378
|
+
}
|
|
379
|
+
case JOURNAL_TYPES.NOTE: {
|
|
380
|
+
templatePath = journalSettings.notePathTemplate;
|
|
381
|
+
mondayDate = currentDate;
|
|
382
|
+
break;
|
|
383
|
+
}
|
|
384
|
+
default:
|
|
385
|
+
throw new Error(`Unknown JournalType: ${type}`);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Resolve the path template and extract directory structure
|
|
389
|
+
const resolvedPath = resolvePathTemplate(templatePath, title, currentDate, mondayDate);
|
|
390
|
+
|
|
391
|
+
// Join baseFolder with the resolved path
|
|
392
|
+
const fullPath = path.join(journalSettings.baseFolder, resolvedPath);
|
|
393
|
+
|
|
394
|
+
return {
|
|
395
|
+
currentDate: currentDate,
|
|
396
|
+
fullPath: fullPath,
|
|
397
|
+
mondayDate,
|
|
398
|
+
} satisfies GenerateJournalFileResult;
|
|
399
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for observe graph command --days filtering
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect } from "vitest";
|
|
5
|
+
import { calculateCutoffMs, filterByDays } from "./graph.js";
|
|
6
|
+
|
|
7
|
+
describe("observe graph --days filtering", () => {
|
|
8
|
+
describe("calculateCutoffMs", () => {
|
|
9
|
+
it("returns 0 when days <= 0", () => {
|
|
10
|
+
expect(calculateCutoffMs(0)).toBe(0);
|
|
11
|
+
expect(calculateCutoffMs(-1)).toBe(0);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("returns cutoff timestamp for positive days", () => {
|
|
15
|
+
const now = Date.now();
|
|
16
|
+
const cutoff = calculateCutoffMs(7);
|
|
17
|
+
// Should be roughly 7 days ago (within 100ms tolerance for test execution time)
|
|
18
|
+
const expected = now - 7 * 24 * 60 * 60 * 1000;
|
|
19
|
+
expect(Math.abs(cutoff - expected)).toBeLessThan(100);
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe("filterByDays", () => {
|
|
24
|
+
it("filters sessions older than N days when days > 0", () => {
|
|
25
|
+
const now = Date.now();
|
|
26
|
+
const sessions = [
|
|
27
|
+
{ mtime: now - 1 * 24 * 60 * 60 * 1000 }, // 1 day ago - included
|
|
28
|
+
{ mtime: now - 2 * 24 * 60 * 60 * 1000 }, // 2 days ago - included
|
|
29
|
+
{ mtime: now - 5 * 24 * 60 * 60 * 1000 }, // 5 days ago - excluded
|
|
30
|
+
{ mtime: now - 10 * 24 * 60 * 60 * 1000 }, // 10 days ago - excluded
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
const filtered = filterByDays(sessions, 3);
|
|
34
|
+
expect(filtered).toHaveLength(2);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("returns all sessions when days=0", () => {
|
|
38
|
+
const now = Date.now();
|
|
39
|
+
const sessions = [
|
|
40
|
+
{ mtime: now - 1 * 24 * 60 * 60 * 1000 },
|
|
41
|
+
{ mtime: now - 100 * 24 * 60 * 60 * 1000 },
|
|
42
|
+
{ mtime: now - 365 * 24 * 60 * 60 * 1000 },
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
const filtered = filterByDays(sessions, 0);
|
|
46
|
+
expect(filtered).toHaveLength(3);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("default 7 days filters correctly", () => {
|
|
50
|
+
const now = Date.now();
|
|
51
|
+
const sessions = [
|
|
52
|
+
{ mtime: now - 1 * 24 * 60 * 60 * 1000 }, // 1 day ago - included
|
|
53
|
+
{ mtime: now - 6 * 24 * 60 * 60 * 1000 }, // 6 days ago - included
|
|
54
|
+
{ mtime: now - 8 * 24 * 60 * 60 * 1000 }, // 8 days ago - excluded
|
|
55
|
+
{ mtime: now - 30 * 24 * 60 * 60 * 1000 }, // 30 days ago - excluded
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
const filtered = filterByDays(sessions, 7);
|
|
59
|
+
expect(filtered).toHaveLength(2);
|
|
60
|
+
// Verify the right sessions were kept
|
|
61
|
+
expect(filtered[0].mtime).toBeGreaterThan(now - 7 * 24 * 60 * 60 * 1000);
|
|
62
|
+
expect(filtered[1].mtime).toBeGreaterThan(now - 7 * 24 * 60 * 60 * 1000);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("--days 1 filters to today only", () => {
|
|
66
|
+
const now = Date.now();
|
|
67
|
+
const sessions = [
|
|
68
|
+
{ mtime: now - 12 * 60 * 60 * 1000 }, // 12 hours ago - included
|
|
69
|
+
{ mtime: now - 25 * 60 * 60 * 1000 }, // 25 hours ago - excluded
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
const filtered = filterByDays(sessions, 1);
|
|
73
|
+
expect(filtered).toHaveLength(1);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("preserves additional properties on items", () => {
|
|
77
|
+
const now = Date.now();
|
|
78
|
+
const sessions = [
|
|
79
|
+
{ mtime: now, sessionId: "abc", tokens: 100 },
|
|
80
|
+
{ mtime: now - 10 * 24 * 60 * 60 * 1000, sessionId: "old", tokens: 50 },
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
const filtered = filterByDays(sessions, 7);
|
|
84
|
+
expect(filtered).toHaveLength(1);
|
|
85
|
+
expect(filtered[0].sessionId).toBe("abc");
|
|
86
|
+
expect(filtered[0].tokens).toBe(100);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
});
|