feat(quick): replace auto-increment number with YYMMDD-xxx timestamp ID (#1012)
Quick task IDs were sequential integers (001, 002...) computed by reading the local .planning/quick/ directory max value. Two users running /gsd:quick simultaneously would get the same number, causing directory collisions on git. Replace with a collision-resistant format: YYMMDD-xxx where xxx is the number of 2-second blocks elapsed since midnight, encoded as 3 lowercase Base36 characters (000–xbz). Practical collision window is ~2 seconds per user — effectively zero for any realistic team workflow. - init.cjs: remove nextNum scan logic, generate quickId from wall clock - quick.md: rename all next_num refs to quick_id, update directory patterns - init.test.cjs: rewrite cmdInitQuick tests for new ID format Co-authored-by: yanbing <yanbing@corp.netease.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -629,15 +629,28 @@ describe('cmdInitQuick', () => {
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
|
||||
test('with description generates slug and task_dir', () => {
|
||||
test('with description generates slug and task_dir with YYMMDD-xxx format', () => {
|
||||
const result = runGsdTools('init quick "Fix login bug"', tmpDir);
|
||||
assert.ok(result.success, `Command failed: ${result.error}`);
|
||||
|
||||
const output = JSON.parse(result.output);
|
||||
assert.strictEqual(output.slug, 'fix-login-bug');
|
||||
assert.strictEqual(output.next_num, 1);
|
||||
assert.strictEqual(output.task_dir, '.planning/quick/1-fix-login-bug');
|
||||
assert.strictEqual(output.description, 'Fix login bug');
|
||||
|
||||
// quick_id must match YYMMDD-xxx (6 digits, dash, 3 base36 chars)
|
||||
assert.ok(/^\d{6}-[0-9a-z]{3}$/.test(output.quick_id),
|
||||
`quick_id should match YYMMDD-xxx, got: "${output.quick_id}"`);
|
||||
|
||||
// task_dir must use the new ID format
|
||||
assert.ok(output.task_dir.startsWith('.planning/quick/'),
|
||||
`task_dir should start with .planning/quick/, got: "${output.task_dir}"`);
|
||||
assert.ok(output.task_dir.endsWith('-fix-login-bug'),
|
||||
`task_dir should end with -fix-login-bug, got: "${output.task_dir}"`);
|
||||
assert.ok(/^\.planning\/quick\/\d{6}-[0-9a-z]{3}-fix-login-bug$/.test(output.task_dir),
|
||||
`task_dir format wrong: "${output.task_dir}"`);
|
||||
|
||||
// next_num must NOT be present
|
||||
assert.ok(!('next_num' in output), 'next_num should not be in output');
|
||||
});
|
||||
|
||||
test('without description returns null slug and task_dir', () => {
|
||||
@@ -648,19 +661,27 @@ describe('cmdInitQuick', () => {
|
||||
assert.strictEqual(output.slug, null);
|
||||
assert.strictEqual(output.task_dir, null);
|
||||
assert.strictEqual(output.description, null);
|
||||
assert.strictEqual(output.next_num, 1);
|
||||
|
||||
// quick_id is still generated even without description
|
||||
assert.ok(/^\d{6}-[0-9a-z]{3}$/.test(output.quick_id),
|
||||
`quick_id should match YYMMDD-xxx, got: "${output.quick_id}"`);
|
||||
});
|
||||
|
||||
test('next number increments from existing entries', () => {
|
||||
const quickDir = path.join(tmpDir, '.planning', 'quick');
|
||||
fs.mkdirSync(path.join(quickDir, '1-old-task'), { recursive: true });
|
||||
fs.mkdirSync(path.join(quickDir, '3-another-task'), { recursive: true });
|
||||
test('two rapid calls produce different quick_ids (no collision within 2s window)', () => {
|
||||
// Both calls happen within the same test, which is sub-second.
|
||||
// They may or may not land in the same 2-second block. We just verify format.
|
||||
const r1 = runGsdTools('init quick "Task one"', tmpDir);
|
||||
const r2 = runGsdTools('init quick "Task two"', tmpDir);
|
||||
assert.ok(r1.success && r2.success);
|
||||
|
||||
const result = runGsdTools('init quick "New task"', tmpDir);
|
||||
assert.ok(result.success, `Command failed: ${result.error}`);
|
||||
const o1 = JSON.parse(r1.output);
|
||||
const o2 = JSON.parse(r2.output);
|
||||
|
||||
const output = JSON.parse(result.output);
|
||||
assert.strictEqual(output.next_num, 4);
|
||||
assert.ok(/^\d{6}-[0-9a-z]{3}$/.test(o1.quick_id));
|
||||
assert.ok(/^\d{6}-[0-9a-z]{3}$/.test(o2.quick_id));
|
||||
|
||||
// Directories are distinct because slugs differ
|
||||
assert.notStrictEqual(o1.task_dir, o2.task_dir);
|
||||
});
|
||||
|
||||
test('long description truncates slug to 40 chars', () => {
|
||||
|
||||
Reference in New Issue
Block a user