@ulysses-ai/create-workspace 0.15.0-beta.0 → 0.15.0-beta.2

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.
Files changed (26) hide show
  1. package/lib/init.mjs +12 -25
  2. package/lib/scaffold.mjs +3 -2
  3. package/package.json +1 -1
  4. package/template/.claude/rules/memory-guidance.md +30 -0
  5. package/template/.claude/scripts/build-workspace-context.mjs +370 -23
  6. package/template/.claude/scripts/migrate-canonical-priority.mjs +108 -0
  7. package/template/.claude/skills/complete-work/SKILL.md +88 -0
  8. package/template/.claude/skills/maintenance/SKILL.md +79 -11
  9. package/template/.claude/skills/release/SKILL.md +3 -0
  10. package/template/.claude/skills/workspace-update/SKILL.md +7 -1
  11. package/template/workspace.json.tmpl +1 -0
  12. package/template/.claude/hooks/_bash-output-advisory.test.mjs +0 -88
  13. package/template/.claude/hooks/_utils.test.mjs +0 -99
  14. package/template/.claude/lib/freshness.test.mjs +0 -175
  15. package/template/.claude/lib/registry-check.test.mjs +0 -130
  16. package/template/.claude/lib/session-frontmatter.test.mjs +0 -242
  17. package/template/.claude/scripts/build-workspace-context.test.mjs +0 -633
  18. package/template/.claude/scripts/capture-context.test.mjs +0 -383
  19. package/template/.claude/scripts/generate-claude-local.test.mjs +0 -184
  20. package/template/.claude/scripts/migrate-claude-md-freshness-include.test.mjs +0 -54
  21. package/template/.claude/scripts/migrate-session-layout.test.mjs +0 -144
  22. package/template/.claude/scripts/migrate-to-workspace-context.test.mjs +0 -325
  23. package/template/.claude/scripts/sweep-references.test.mjs +0 -184
  24. package/template/.claude/scripts/sync-tasks.test.mjs +0 -350
  25. package/template/.claude/scripts/trackers/github-issues.test.mjs +0 -190
  26. package/template/.claude/scripts/trackers/interface.test.mjs +0 -40
@@ -1,175 +0,0 @@
1
- #!/usr/bin/env node
2
- // Unit tests for freshness.mjs
3
- // Run: node template/.claude/lib/freshness.test.mjs
4
- import { refreshIfStale } from './freshness.mjs';
5
- import { mkdtempSync, rmSync, writeFileSync, existsSync, readFileSync, mkdirSync } from 'fs';
6
- import { join } from 'path';
7
- import { tmpdir } from 'os';
8
-
9
- let failed = 0;
10
- let passed = 0;
11
- function assertEq(actual, expected, msg) {
12
- const a = JSON.stringify(actual);
13
- const e = JSON.stringify(expected);
14
- if (a === e) { passed++; } else {
15
- failed++;
16
- console.error(` FAIL: ${msg}\n expected: ${e}\n actual: ${a}`);
17
- }
18
- }
19
-
20
- function setupWorkspace({ templateVersion, ambientBlock = '', cache = null } = {}) {
21
- const root = mkdtempSync(join(tmpdir(), 'freshness-test-'));
22
- const wsConfig = {
23
- workspace: {
24
- name: 'test',
25
- scratchpadDir: 'workspace-scratchpad',
26
- templateVersion,
27
- ...(ambientBlock ? { versionCheck: { ambient: ambientBlock === 'on' } } : {}),
28
- },
29
- repos: {},
30
- };
31
- writeFileSync(join(root, 'workspace.json'), JSON.stringify(wsConfig));
32
- if (cache) {
33
- const cacheDir = join(root, 'workspace-scratchpad');
34
- mkdirSync(cacheDir, { recursive: true });
35
- writeFileSync(join(cacheDir, '.version-check.json'), JSON.stringify(cache));
36
- }
37
- return root;
38
- }
39
-
40
- function fakeFetchOk(version) {
41
- return async () => ({ ok: true, json: async () => ({ version }) });
42
- }
43
-
44
- const fakeFetchErr = async () => { throw new Error('offline'); };
45
-
46
- console.log('# refreshIfStale');
47
-
48
- // Outdated workspace, fresh fetch, banner written
49
- {
50
- const root = setupWorkspace({ templateVersion: '0.13.0' });
51
- const result = await refreshIfStale({
52
- workspaceRoot: root,
53
- ttlMs: 86400000,
54
- fetchFn: fakeFetchOk('0.14.0'),
55
- nowFn: () => new Date('2026-04-24T21:00:00Z'),
56
- });
57
- assertEq(result.status, 'outdated', 'outdated status');
58
- assertEq(result.current, '0.13.0', 'current version reported');
59
- assertEq(result.latest, '0.14.0', 'latest version reported');
60
- assertEq(existsSync(join(root, 'local-only-template-freshness.md')), true, 'banner file created');
61
- const banner = readFileSync(join(root, 'local-only-template-freshness.md'), 'utf-8');
62
- assertEq(banner.includes('v0.13.0'), true, 'banner mentions current');
63
- assertEq(banner.includes('v0.14.0'), true, 'banner mentions latest');
64
- rmSync(root, { recursive: true, force: true });
65
- }
66
-
67
- // Up-to-date workspace, banner deleted if present
68
- {
69
- const root = setupWorkspace({ templateVersion: '0.14.0' });
70
- // Pre-existing banner from when it was outdated
71
- writeFileSync(join(root, 'local-only-template-freshness.md'), '## stale banner');
72
- await refreshIfStale({
73
- workspaceRoot: root,
74
- ttlMs: 86400000,
75
- fetchFn: fakeFetchOk('0.14.0'),
76
- nowFn: () => new Date('2026-04-24T21:00:00Z'),
77
- });
78
- assertEq(existsSync(join(root, 'local-only-template-freshness.md')), false, 'banner deleted when current');
79
- rmSync(root, { recursive: true, force: true });
80
- }
81
-
82
- // Fresh cache, no fetch happens
83
- {
84
- const root = setupWorkspace({
85
- templateVersion: '0.13.0',
86
- cache: { latestVersion: '0.14.0', checkedAt: '2026-04-24T20:00:00Z' },
87
- });
88
- let fetchCalled = false;
89
- await refreshIfStale({
90
- workspaceRoot: root,
91
- ttlMs: 86400000,
92
- fetchFn: async () => { fetchCalled = true; return { ok: true, json: async () => ({ version: '0.14.0' }) }; },
93
- nowFn: () => new Date('2026-04-24T21:00:00Z'), // 1h after cache
94
- });
95
- assertEq(fetchCalled, false, 'fresh cache skips fetch');
96
- assertEq(existsSync(join(root, 'local-only-template-freshness.md')), true, 'banner still written from cache');
97
- rmSync(root, { recursive: true, force: true });
98
- }
99
-
100
- // Stale cache triggers fetch
101
- {
102
- const root = setupWorkspace({
103
- templateVersion: '0.13.0',
104
- cache: { latestVersion: '0.13.5', checkedAt: '2026-04-20T20:00:00Z' },
105
- });
106
- let fetchCalled = false;
107
- await refreshIfStale({
108
- workspaceRoot: root,
109
- ttlMs: 86400000,
110
- fetchFn: async () => { fetchCalled = true; return { ok: true, json: async () => ({ version: '0.14.0' }) }; },
111
- nowFn: () => new Date('2026-04-24T21:00:00Z'),
112
- });
113
- assertEq(fetchCalled, true, 'stale cache triggers fetch');
114
- rmSync(root, { recursive: true, force: true });
115
- }
116
-
117
- // Stale cache + offline: keep cached value, return unknown only if no cache
118
- {
119
- const root = setupWorkspace({
120
- templateVersion: '0.13.0',
121
- cache: { latestVersion: '0.13.9', checkedAt: '2026-04-20T20:00:00Z' },
122
- });
123
- const result = await refreshIfStale({
124
- workspaceRoot: root,
125
- ttlMs: 86400000,
126
- fetchFn: fakeFetchErr,
127
- nowFn: () => new Date('2026-04-24T21:00:00Z'),
128
- });
129
- assertEq(result.status, 'outdated', 'falls back to cached value when offline');
130
- assertEq(result.latest, '0.13.9', 'cached value used');
131
- rmSync(root, { recursive: true, force: true });
132
- }
133
-
134
- // No cache + offline: status unknown
135
- {
136
- const root = setupWorkspace({ templateVersion: '0.13.0' });
137
- const result = await refreshIfStale({
138
- workspaceRoot: root,
139
- ttlMs: 86400000,
140
- fetchFn: fakeFetchErr,
141
- nowFn: () => new Date('2026-04-24T21:00:00Z'),
142
- });
143
- assertEq(result.status, 'unknown', 'unknown when no cache and offline');
144
- rmSync(root, { recursive: true, force: true });
145
- }
146
-
147
- // Uninitialized workspace (templateVersion missing)
148
- {
149
- const root = mkdtempSync(join(tmpdir(), 'freshness-test-'));
150
- writeFileSync(join(root, 'workspace.json'), JSON.stringify({ workspace: {}, repos: {} }));
151
- const result = await refreshIfStale({
152
- workspaceRoot: root,
153
- ttlMs: 86400000,
154
- fetchFn: fakeFetchOk('0.14.0'),
155
- nowFn: () => new Date('2026-04-24T21:00:00Z'),
156
- });
157
- assertEq(result.skipped, 'uninitialized', 'uninitialized workspace skipped');
158
- rmSync(root, { recursive: true, force: true });
159
- }
160
-
161
- // templateVersion 0.0.0 treated as uninitialized
162
- {
163
- const root = setupWorkspace({ templateVersion: '0.0.0' });
164
- const result = await refreshIfStale({
165
- workspaceRoot: root,
166
- ttlMs: 86400000,
167
- fetchFn: fakeFetchOk('0.14.0'),
168
- nowFn: () => new Date('2026-04-24T21:00:00Z'),
169
- });
170
- assertEq(result.skipped, 'uninitialized', '0.0.0 treated as uninitialized');
171
- rmSync(root, { recursive: true, force: true });
172
- }
173
-
174
- console.log(`\n${passed} passed, ${failed} failed`);
175
- process.exit(failed > 0 ? 1 : 0);
@@ -1,130 +0,0 @@
1
- #!/usr/bin/env node
2
- // Unit tests for registry-check.mjs
3
- // Run: node template/.claude/lib/registry-check.test.mjs
4
- import { compareVersions } from './registry-check.mjs';
5
-
6
- let failed = 0;
7
- let passed = 0;
8
-
9
- function assertEq(actual, expected, msg) {
10
- const a = JSON.stringify(actual);
11
- const e = JSON.stringify(expected);
12
- if (a === e) { passed++; } else {
13
- failed++;
14
- console.error(` FAIL: ${msg}\n expected: ${e}\n actual: ${a}`);
15
- }
16
- }
17
-
18
- console.log('# compareVersions');
19
- assertEq(compareVersions('0.13.0', '0.14.0'), -1, 'older minor');
20
- assertEq(compareVersions('0.14.0', '0.13.0'), 1, 'newer minor');
21
- assertEq(compareVersions('0.13.0', '0.13.0'), 0, 'equal');
22
- assertEq(compareVersions('1.0.0', '0.99.99'), 1, 'newer major');
23
- assertEq(compareVersions('0.14.0-beta.1', '0.14.0'), -1, 'prerelease < release');
24
- assertEq(compareVersions('0.14.0', '0.14.0-beta.1'), 1, 'release > prerelease');
25
- assertEq(compareVersions('0.14.0-beta.1', '0.14.0-beta.2'), -1, 'prerelease numeric ordering');
26
- assertEq(compareVersions('0.14.0-beta.10', '0.14.0-beta.2'), 1, 'prerelease numeric not lexical');
27
- assertEq(compareVersions('0.14.0-beta.5', '0.14.0-beta.5'), 0, 'equal prerelease');
28
- assertEq(compareVersions('0.14.0-alpha.1', '0.14.0-beta.1'),-1, 'alpha < beta lexical');
29
-
30
- import { getLatestVersion } from './registry-check.mjs';
31
-
32
- console.log('\n# getLatestVersion');
33
-
34
- // Helper: build a fake fetch
35
- function fakeFetch(response) {
36
- return async () => response;
37
- }
38
-
39
- // Success path
40
- {
41
- const fakeOk = fakeFetch({
42
- ok: true,
43
- json: async () => ({ version: '0.14.0', name: '@ulysses-ai/create-workspace' }),
44
- });
45
- const result = await getLatestVersion({ fetchFn: fakeOk });
46
- assertEq(result, { version: '0.14.0', error: null }, 'success returns version');
47
- }
48
-
49
- // Non-2xx response
50
- {
51
- const fake404 = fakeFetch({ ok: false, status: 404, statusText: 'Not Found' });
52
- const result = await getLatestVersion({ fetchFn: fake404 });
53
- assertEq(result.version, null, 'non-2xx version is null');
54
- assertEq(typeof result.error, 'string', 'non-2xx returns error string');
55
- }
56
-
57
- // Malformed body (no version field)
58
- {
59
- const fakeBad = fakeFetch({ ok: true, json: async () => ({ name: 'foo' }) });
60
- const result = await getLatestVersion({ fetchFn: fakeBad });
61
- assertEq(result.version, null, 'missing version field is null');
62
- assertEq(typeof result.error, 'string', 'missing version returns error');
63
- }
64
-
65
- // Network error (fetch throws)
66
- {
67
- const fakeThrow = async () => { throw new Error('ECONNREFUSED'); };
68
- const result = await getLatestVersion({ fetchFn: fakeThrow });
69
- assertEq(result.version, null, 'thrown error returns null version');
70
- assertEq(result.error.includes('ECONNREFUSED'), true, 'thrown error message preserved');
71
- }
72
-
73
- import { readCache, writeCache } from './registry-check.mjs';
74
- import { mkdtempSync, rmSync, writeFileSync } from 'fs';
75
- import { join } from 'path';
76
- import { tmpdir } from 'os';
77
-
78
- console.log('\n# readCache / writeCache');
79
-
80
- function withTempDir(fn) {
81
- const dir = mkdtempSync(join(tmpdir(), 'reg-cache-test-'));
82
- try { fn(dir); } finally { rmSync(dir, { recursive: true, force: true }); }
83
- }
84
-
85
- // readCache: missing file returns null
86
- withTempDir((dir) => {
87
- const result = readCache(join(dir, 'missing.json'));
88
- assertEq(result, null, 'missing file returns null');
89
- });
90
-
91
- // readCache: malformed JSON returns null
92
- withTempDir((dir) => {
93
- const path = join(dir, 'bad.json');
94
- writeFileSync(path, '{not json');
95
- const result = readCache(path);
96
- assertEq(result, null, 'malformed JSON returns null');
97
- });
98
-
99
- // readCache: valid file returns parsed object
100
- withTempDir((dir) => {
101
- const path = join(dir, 'good.json');
102
- writeFileSync(path, JSON.stringify({ latestVersion: '0.14.0', checkedAt: '2026-04-24T21:00:00Z' }));
103
- const result = readCache(path);
104
- assertEq(result, { latestVersion: '0.14.0', checkedAt: '2026-04-24T21:00:00Z' }, 'valid file parsed');
105
- });
106
-
107
- // readCache: missing required fields returns null
108
- withTempDir((dir) => {
109
- const path = join(dir, 'partial.json');
110
- writeFileSync(path, JSON.stringify({ checkedAt: '2026-04-24T21:00:00Z' }));
111
- const result = readCache(path);
112
- assertEq(result, null, 'missing latestVersion returns null');
113
- });
114
-
115
- // writeCache + readCache round-trip
116
- withTempDir((dir) => {
117
- const path = join(dir, 'rt.json');
118
- writeCache(path, { latestVersion: '0.14.0', checkedAt: '2026-04-24T21:00:00Z' });
119
- assertEq(readCache(path), { latestVersion: '0.14.0', checkedAt: '2026-04-24T21:00:00Z' }, 'round-trip equal');
120
- });
121
-
122
- // writeCache creates parent dir if missing
123
- withTempDir((dir) => {
124
- const path = join(dir, 'nested', 'deep', 'cache.json');
125
- writeCache(path, { latestVersion: '0.14.0', checkedAt: '2026-04-24T21:00:00Z' });
126
- assertEq(readCache(path)?.latestVersion, '0.14.0', 'parent dir created');
127
- });
128
-
129
- console.log(`\n${passed} passed, ${failed} failed`);
130
- process.exit(failed > 0 ? 1 : 0);
@@ -1,242 +0,0 @@
1
- #!/usr/bin/env node
2
- // Unit tests for session-frontmatter.mjs
3
- // Run: node template/.claude/lib/session-frontmatter.test.mjs
4
- import {
5
- parseSessionContent,
6
- updateSessionContent,
7
- } from './session-frontmatter.mjs';
8
-
9
- let failed = 0;
10
- let passed = 0;
11
-
12
- function assert(cond, msg) {
13
- if (cond) {
14
- passed++;
15
- } else {
16
- failed++;
17
- console.error(` FAIL: ${msg}`);
18
- }
19
- }
20
-
21
- function assertEq(actual, expected, msg) {
22
- const a = JSON.stringify(actual);
23
- const e = JSON.stringify(expected);
24
- if (a === e) {
25
- passed++;
26
- } else {
27
- failed++;
28
- console.error(` FAIL: ${msg}\n expected: ${e}\n actual: ${a}`);
29
- }
30
- }
31
-
32
- const SAMPLE = `---
33
- type: session-tracker
34
- name: fix-auth
35
- description: Fix the auth timeout on mobile
36
- status: active
37
- branch: bugfix/fix-auth
38
- created: 2026-04-13T05:33:50.000Z
39
- user: alice
40
- repos:
41
- - my-app
42
- - my-api
43
- workItem: 3
44
- chatSessions:
45
- - id: aa3c952e-dbff-4055-8bcc-e5f217618d57
46
- names: [pickup-from-braindump]
47
- started: 2026-04-13T05:33:50.000Z
48
- ended: 2026-04-13T07:12:00.000Z
49
- - id: bb4d063e-ec00-4166-cc71-cd3d3d4628a1
50
- names: []
51
- started: 2026-04-13T08:00:00.000Z
52
- ended: null
53
- author: alice
54
- updated: 2026-04-13
55
- ---
56
-
57
- # Work Session: fix-auth
58
-
59
- Brief one-liner.
60
-
61
- ## Progress
62
-
63
- Some prose.
64
- `;
65
-
66
- // === Test 1: parse all field types ===
67
- console.log('Test 1: parse all field types');
68
- {
69
- const parsed = parseSessionContent(SAMPLE);
70
- assertEq(parsed.fields.type, 'session-tracker', 'type');
71
- assertEq(parsed.fields.name, 'fix-auth', 'name');
72
- assertEq(parsed.fields.description, 'Fix the auth timeout on mobile', 'description');
73
- assertEq(parsed.fields.status, 'active', 'status');
74
- assertEq(parsed.fields.branch, 'bugfix/fix-auth', 'branch');
75
- assertEq(parsed.fields.created, '2026-04-13T05:33:50.000Z', 'created (ISO timestamp unquoted)');
76
- assertEq(parsed.fields.workItem, 3, 'workItem (integer)');
77
- assertEq(parsed.fields.repos, ['my-app', 'my-api'], 'repos (flat list)');
78
- assertEq(parsed.fields.chatSessions.length, 2, 'chatSessions length');
79
- assertEq(parsed.fields.chatSessions[0].id, 'aa3c952e-dbff-4055-8bcc-e5f217618d57', 'chat 0 id');
80
- assertEq(parsed.fields.chatSessions[0].names, ['pickup-from-braindump'], 'chat 0 names');
81
- assertEq(parsed.fields.chatSessions[0].started, '2026-04-13T05:33:50.000Z', 'chat 0 started');
82
- assertEq(parsed.fields.chatSessions[0].ended, '2026-04-13T07:12:00.000Z', 'chat 0 ended');
83
- assertEq(parsed.fields.chatSessions[1].id, 'bb4d063e-ec00-4166-cc71-cd3d3d4628a1', 'chat 1 id');
84
- assertEq(parsed.fields.chatSessions[1].names, [], 'chat 1 names (empty inline list)');
85
- assertEq(parsed.fields.chatSessions[1].ended, null, 'chat 1 ended (null)');
86
- assert(parsed.body.startsWith('\n# Work Session'), 'body starts with blank line + H1');
87
- assert(parsed.body.endsWith('Some prose.\n'), 'body ends with trailing newline');
88
- }
89
-
90
- // === Test 2: lossless byte-identity on no-op update ===
91
- console.log('Test 2: lossless byte-identity on no-op update');
92
- {
93
- const unchanged = updateSessionContent(SAMPLE, {});
94
- assertEq(unchanged, SAMPLE, 'no-op update preserves content byte-identical');
95
- }
96
-
97
- // === Test 3: update single scalar field ===
98
- console.log('Test 3: update single scalar field');
99
- {
100
- const updated = updateSessionContent(SAMPLE, { status: 'paused' });
101
- const parsed = parseSessionContent(updated);
102
- assertEq(parsed.fields.status, 'paused', 'status changed');
103
- assertEq(parsed.fields.name, 'fix-auth', 'name preserved');
104
- assertEq(parsed.fields.repos, ['my-app', 'my-api'], 'repos preserved');
105
- assertEq(parsed.fields.chatSessions.length, 2, 'chatSessions preserved');
106
- // Verify every other line is byte-identical
107
- const origLines = SAMPLE.split('\n');
108
- const newLines = updated.split('\n');
109
- assertEq(newLines.length, origLines.length, 'same number of lines');
110
- for (let i = 0; i < origLines.length; i++) {
111
- if (origLines[i].startsWith('status:')) {
112
- assertEq(newLines[i], 'status: paused', `line ${i} is new status`);
113
- } else {
114
- assertEq(newLines[i], origLines[i], `line ${i} preserved`);
115
- }
116
- }
117
- }
118
-
119
- // === Test 4: update chatSessions array (append a chat) ===
120
- console.log('Test 4: update chatSessions array');
121
- {
122
- const parsed = parseSessionContent(SAMPLE);
123
- const newChats = [...parsed.fields.chatSessions, {
124
- id: 'cc5e174f-fd11-5277-dd82-de4e4e5739b2',
125
- names: [],
126
- started: '2026-04-13T09:00:00.000Z',
127
- ended: null,
128
- }];
129
- const updated = updateSessionContent(SAMPLE, { chatSessions: newChats });
130
- const reparsed = parseSessionContent(updated);
131
- assertEq(reparsed.fields.chatSessions.length, 3, 'chatSessions now has 3 entries');
132
- assertEq(reparsed.fields.chatSessions[2].id, 'cc5e174f-fd11-5277-dd82-de4e4e5739b2', 'new chat id');
133
- assertEq(reparsed.fields.name, 'fix-auth', 'other fields preserved');
134
- assertEq(reparsed.body, parsed.body, 'body preserved byte-identical');
135
- }
136
-
137
- // === Test 5: append a new field ===
138
- console.log('Test 5: append a new field');
139
- {
140
- const updated = updateSessionContent(SAMPLE, { newField: 'hello' });
141
- const parsed = parseSessionContent(updated);
142
- assertEq(parsed.fields.newField, 'hello', 'newField present');
143
- assertEq(parsed.fields.name, 'fix-auth', 'existing field preserved');
144
- }
145
-
146
- // === Test 6: remove a field ===
147
- console.log('Test 6: remove a field');
148
- {
149
- const updated = updateSessionContent(SAMPLE, { workItem: undefined });
150
- const parsed = parseSessionContent(updated);
151
- assertEq(parsed.fields.workItem, undefined, 'workItem removed');
152
- assertEq(parsed.fields.user, 'alice', 'user preserved');
153
- }
154
-
155
- // === Test 7: update repos (flat list) ===
156
- console.log('Test 7: update repos flat list');
157
- {
158
- const updated = updateSessionContent(SAMPLE, { repos: ['my-app', 'my-api', 'my-web'] });
159
- const parsed = parseSessionContent(updated);
160
- assertEq(parsed.fields.repos, ['my-app', 'my-api', 'my-web'], 'repos has 3 items');
161
- assertEq(parsed.fields.chatSessions.length, 2, 'chatSessions preserved after repos update');
162
- }
163
-
164
- // === Test 8: empty repos ===
165
- console.log('Test 8: empty repos');
166
- {
167
- const updated = updateSessionContent(SAMPLE, { repos: [] });
168
- const parsed = parseSessionContent(updated);
169
- assertEq(parsed.fields.repos, [], 'repos is empty');
170
- }
171
-
172
- // === Test 9: scalar with special chars (colon in branch name) ===
173
- console.log('Test 9: scalar with special chars');
174
- {
175
- // Branch names with slashes should NOT be quoted
176
- const sample = `---
177
- branch: feature/worksessions-refactor
178
- ---
179
- body
180
- `;
181
- const parsed = parseSessionContent(sample);
182
- assertEq(parsed.fields.branch, 'feature/worksessions-refactor', 'branch unquoted');
183
- const noop = updateSessionContent(sample, {});
184
- assertEq(noop, sample, 'no-op preserves unquoted slash');
185
- }
186
-
187
- // === Test 10: ISO timestamp round-trip ===
188
- console.log('Test 10: ISO timestamp round-trip');
189
- {
190
- const sample = `---
191
- created: 2026-04-13T05:33:50.000Z
192
- ---
193
- body
194
- `;
195
- const parsed = parseSessionContent(sample);
196
- assertEq(parsed.fields.created, '2026-04-13T05:33:50.000Z', 'ISO timestamp parsed');
197
- const updated = updateSessionContent(sample, { created: '2026-05-01T12:00:00.000Z' });
198
- const reparsed = parseSessionContent(updated);
199
- assertEq(reparsed.fields.created, '2026-05-01T12:00:00.000Z', 'ISO timestamp round-trip');
200
- // Must not be quoted
201
- assert(!updated.includes('"2026-05-01'), 'ISO timestamp not quoted on write');
202
- }
203
-
204
- // === Test 11: string that truly needs quoting ===
205
- console.log('Test 11: string that truly needs quoting');
206
- {
207
- const sample = `---
208
- title: hello
209
- ---
210
- body
211
- `;
212
- const updated = updateSessionContent(sample, { title: 'hello: world' });
213
- assert(updated.includes('title: "hello: world"'), 'colon-space triggers quoting');
214
- const reparsed = parseSessionContent(updated);
215
- assertEq(reparsed.fields.title, 'hello: world', 'quoted value round-trips');
216
- }
217
-
218
- // === Test 12: body preservation with multiline content ===
219
- console.log('Test 12: body preservation with multiline content');
220
- {
221
- const sample = `---
222
- name: test
223
- ---
224
-
225
- # Heading
226
-
227
- Paragraph with multiple lines.
228
- And more lines.
229
-
230
- - bullet 1
231
- - bullet 2
232
- `;
233
- const noop = updateSessionContent(sample, {});
234
- assertEq(noop, sample, 'multiline body preserved byte-identical');
235
- const updated = updateSessionContent(sample, { name: 'changed' });
236
- const parsed = parseSessionContent(updated);
237
- assertEq(parsed.body.trim(), '# Heading\n\nParagraph with multiple lines.\nAnd more lines.\n\n- bullet 1\n- bullet 2', 'body intact after update');
238
- }
239
-
240
- // === Summary ===
241
- console.log(`\n${passed} passed, ${failed} failed`);
242
- if (failed > 0) process.exit(1);