fix: display relative time in intel status output (#2132)
* fix: display relative time instead of UTC in intel status output
The `updated_at` timestamps in `gsd-tools intel status` were displayed
as raw ISO/UTC strings, making them appear to show the wrong time in
non-UTC timezones. Replace with fuzzy relative times ("5 minutes ago",
"1 day ago") which are timezone-agnostic and more useful for freshness.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: add regression tests for timeAgo utility
Covers boundary values (seconds/minutes/hours/days/months/years),
singular vs plural formatting, and future-date edge case.
Addresses review feedback on #2132.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1045,7 +1045,15 @@ async function runCommand(command, args, cwd, raw, defaultValue) {
|
||||
core.output(intel.intelQuery(term, planningDir), raw);
|
||||
} else if (subcommand === 'status') {
|
||||
const planningDir = path.join(cwd, '.planning');
|
||||
core.output(intel.intelStatus(planningDir), raw);
|
||||
const status = intel.intelStatus(planningDir);
|
||||
if (!raw && status.files) {
|
||||
for (const file of Object.values(status.files)) {
|
||||
if (file.updated_at) {
|
||||
file.updated_at = core.timeAgo(new Date(file.updated_at));
|
||||
}
|
||||
}
|
||||
}
|
||||
core.output(status, raw);
|
||||
} else if (subcommand === 'diff') {
|
||||
const planningDir = path.join(cwd, '.planning');
|
||||
core.output(intel.intelDiff(planningDir), raw);
|
||||
|
||||
@@ -1560,6 +1560,32 @@ function atomicWriteFileSync(filePath, content, encoding = 'utf-8') {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a Date as a fuzzy relative time string (e.g. "5 minutes ago").
|
||||
* @param {Date} date
|
||||
* @returns {string}
|
||||
*/
|
||||
function timeAgo(date) {
|
||||
const seconds = Math.floor((Date.now() - date.getTime()) / 1000);
|
||||
if (seconds < 5) return 'just now';
|
||||
if (seconds < 60) return `${seconds} seconds ago`;
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
if (minutes === 1) return '1 minute ago';
|
||||
if (minutes < 60) return `${minutes} minutes ago`;
|
||||
const hours = Math.floor(minutes / 60);
|
||||
if (hours === 1) return '1 hour ago';
|
||||
if (hours < 24) return `${hours} hours ago`;
|
||||
const days = Math.floor(hours / 24);
|
||||
if (days === 1) return '1 day ago';
|
||||
if (days < 30) return `${days} days ago`;
|
||||
const months = Math.floor(days / 30);
|
||||
if (months === 1) return '1 month ago';
|
||||
if (months < 12) return `${months} months ago`;
|
||||
const years = Math.floor(days / 365);
|
||||
if (years === 1) return '1 year ago';
|
||||
return `${years} years ago`;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
output,
|
||||
error,
|
||||
@@ -1607,4 +1633,5 @@ module.exports = {
|
||||
getAgentsDir,
|
||||
checkAgentsInstalled,
|
||||
atomicWriteFileSync,
|
||||
timeAgo,
|
||||
};
|
||||
|
||||
@@ -31,6 +31,7 @@ const {
|
||||
findProjectRoot,
|
||||
detectSubRepos,
|
||||
planningDir,
|
||||
timeAgo,
|
||||
} = require('../get-shit-done/bin/lib/core.cjs');
|
||||
|
||||
// ─── loadConfig ────────────────────────────────────────────────────────────────
|
||||
@@ -1750,3 +1751,103 @@ describe('planningDir', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── timeAgo ──────────────────────────────────────────────────────────────────
|
||||
|
||||
describe('timeAgo', () => {
|
||||
const now = () => Date.now();
|
||||
const dateAt = (msAgo) => new Date(now() - msAgo);
|
||||
|
||||
// ─── seconds boundary ───
|
||||
test('returns "just now" for dates under 5 seconds old', () => {
|
||||
assert.strictEqual(timeAgo(dateAt(0)), 'just now');
|
||||
assert.strictEqual(timeAgo(dateAt(4_000)), 'just now');
|
||||
});
|
||||
|
||||
test('returns "N seconds ago" between 5 and 59 seconds', () => {
|
||||
assert.strictEqual(timeAgo(dateAt(5_000)), '5 seconds ago');
|
||||
assert.strictEqual(timeAgo(dateAt(30_000)), '30 seconds ago');
|
||||
assert.strictEqual(timeAgo(dateAt(59_000)), '59 seconds ago');
|
||||
});
|
||||
|
||||
// ─── minutes boundary ───
|
||||
test('transitions to minutes at 60 seconds', () => {
|
||||
assert.strictEqual(timeAgo(dateAt(60_000)), '1 minute ago');
|
||||
});
|
||||
|
||||
test('uses singular "1 minute ago" for exactly one minute', () => {
|
||||
assert.strictEqual(timeAgo(dateAt(60_000)), '1 minute ago');
|
||||
assert.strictEqual(timeAgo(dateAt(119_000)), '1 minute ago');
|
||||
});
|
||||
|
||||
test('uses plural "N minutes ago" for 2-59 minutes', () => {
|
||||
assert.strictEqual(timeAgo(dateAt(120_000)), '2 minutes ago');
|
||||
assert.strictEqual(timeAgo(dateAt(5 * 60_000)), '5 minutes ago');
|
||||
assert.strictEqual(timeAgo(dateAt(59 * 60_000)), '59 minutes ago');
|
||||
});
|
||||
|
||||
// ─── hours boundary ───
|
||||
test('transitions to hours at 60 minutes', () => {
|
||||
assert.strictEqual(timeAgo(dateAt(60 * 60_000)), '1 hour ago');
|
||||
});
|
||||
|
||||
test('uses singular "1 hour ago" for exactly one hour', () => {
|
||||
assert.strictEqual(timeAgo(dateAt(60 * 60_000)), '1 hour ago');
|
||||
assert.strictEqual(timeAgo(dateAt(119 * 60_000)), '1 hour ago');
|
||||
});
|
||||
|
||||
test('uses plural "N hours ago" for 2-23 hours', () => {
|
||||
assert.strictEqual(timeAgo(dateAt(2 * 3600_000)), '2 hours ago');
|
||||
assert.strictEqual(timeAgo(dateAt(23 * 3600_000)), '23 hours ago');
|
||||
});
|
||||
|
||||
// ─── days boundary ───
|
||||
test('transitions to days at 24 hours', () => {
|
||||
assert.strictEqual(timeAgo(dateAt(24 * 3600_000)), '1 day ago');
|
||||
});
|
||||
|
||||
test('uses singular "1 day ago" for exactly one day', () => {
|
||||
assert.strictEqual(timeAgo(dateAt(24 * 3600_000)), '1 day ago');
|
||||
});
|
||||
|
||||
test('uses plural "N days ago" for 2-29 days', () => {
|
||||
assert.strictEqual(timeAgo(dateAt(2 * 86400_000)), '2 days ago');
|
||||
assert.strictEqual(timeAgo(dateAt(29 * 86400_000)), '29 days ago');
|
||||
});
|
||||
|
||||
// ─── months boundary ───
|
||||
test('transitions to months at 30 days', () => {
|
||||
assert.strictEqual(timeAgo(dateAt(30 * 86400_000)), '1 month ago');
|
||||
});
|
||||
|
||||
test('uses singular "1 month ago" for exactly one month (30 days)', () => {
|
||||
assert.strictEqual(timeAgo(dateAt(30 * 86400_000)), '1 month ago');
|
||||
assert.strictEqual(timeAgo(dateAt(59 * 86400_000)), '1 month ago');
|
||||
});
|
||||
|
||||
test('uses plural "N months ago" for 2-11 months', () => {
|
||||
assert.strictEqual(timeAgo(dateAt(60 * 86400_000)), '2 months ago');
|
||||
assert.strictEqual(timeAgo(dateAt(180 * 86400_000)), '6 months ago');
|
||||
});
|
||||
|
||||
// ─── years boundary ───
|
||||
test('transitions to years at 365 days', () => {
|
||||
assert.strictEqual(timeAgo(dateAt(365 * 86400_000)), '1 year ago');
|
||||
});
|
||||
|
||||
test('uses singular "1 year ago" for exactly one year', () => {
|
||||
assert.strictEqual(timeAgo(dateAt(365 * 86400_000)), '1 year ago');
|
||||
});
|
||||
|
||||
test('uses plural "N years ago" for 2+ years', () => {
|
||||
assert.strictEqual(timeAgo(dateAt(2 * 365 * 86400_000)), '2 years ago');
|
||||
assert.strictEqual(timeAgo(dateAt(10 * 365 * 86400_000)), '10 years ago');
|
||||
});
|
||||
|
||||
// ─── edge cases ───
|
||||
test('handles future dates as "just now" (negative elapsed)', () => {
|
||||
// A date 5 seconds in the future has negative elapsed time, which floors to a negative
|
||||
// number of seconds and hits the "under 5 seconds" branch.
|
||||
assert.strictEqual(timeAgo(new Date(Date.now() + 5_000)), 'just now');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user