tissues 0.6.1 → 0.6.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 (42) hide show
  1. package/README.md +78 -1
  2. package/package.json +4 -2
  3. package/src/cli.js +10 -1
  4. package/src/commands/ai.js +147 -173
  5. package/src/commands/create.js +6 -6
  6. package/src/commands/create.test.js +381 -0
  7. package/src/commands/flush.test.js +299 -0
  8. package/src/commands/list.js +3 -2
  9. package/src/commands/providers.js +347 -0
  10. package/src/commands/providers.test.js +28 -0
  11. package/src/commands/storage.js +167 -0
  12. package/src/commands/sync.js +225 -0
  13. package/src/daemon/sync.js +189 -0
  14. package/src/lib/ai/adapters/claude-cli.js +55 -0
  15. package/src/lib/ai/adapters/cli-adapters.test.js +133 -0
  16. package/src/lib/ai/adapters/codex-cli.js +77 -0
  17. package/src/lib/ai/adapters/gemini-cli.js +55 -0
  18. package/src/lib/ai/adapters/openclaw.js +91 -0
  19. package/src/lib/ai/agent-actions.js +271 -0
  20. package/src/lib/ai/agent.js +323 -0
  21. package/src/lib/ai/discovery.js +89 -0
  22. package/src/lib/ai/discovery.test.js +74 -0
  23. package/src/lib/ai/enhancement-adapter.test.js +188 -0
  24. package/src/lib/ai/pipeline.test.js +257 -0
  25. package/src/lib/ai/prompt.test.js +30 -0
  26. package/src/lib/ai/router.js +23 -0
  27. package/src/lib/ai/router.test.js +481 -0
  28. package/src/lib/ai/steps.test.js +335 -0
  29. package/src/lib/attribution.test.js +64 -0
  30. package/src/lib/cache.js +408 -0
  31. package/src/lib/db.js +42 -0
  32. package/src/lib/dedup.js +8 -18
  33. package/src/lib/dedup.test.js +227 -0
  34. package/src/lib/defaults.js +20 -0
  35. package/src/lib/defaults.test.js +217 -0
  36. package/src/lib/drafts-perf.test.js +203 -0
  37. package/src/lib/drafts.test.js +300 -0
  38. package/src/lib/enhancements.test.js +294 -0
  39. package/src/lib/gh.js +60 -0
  40. package/src/lib/safety.test.js +217 -0
  41. package/src/lib/storage.js +298 -0
  42. package/src/lib/templates.test.js +207 -0
@@ -0,0 +1,207 @@
1
+ import { test, describe } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import fs from 'node:fs'
4
+ import path from 'node:path'
5
+ import os from 'node:os'
6
+ import { listTemplates, loadTemplate, renderTemplate } from './templates.js'
7
+
8
+ // ---------------------------------------------------------------------------
9
+ // renderTemplate — pure function, no file I/O
10
+ // ---------------------------------------------------------------------------
11
+
12
+ describe('renderTemplate', () => {
13
+ test('replaces a single variable', () => {
14
+ const result = renderTemplate('Hello {{name}}!', { name: 'World' })
15
+ assert.equal(result, 'Hello World!')
16
+ })
17
+
18
+ test('replaces multiple different variables', () => {
19
+ const result = renderTemplate('{{greeting}} {{name}}', { greeting: 'Hi', name: 'Alice' })
20
+ assert.equal(result, 'Hi Alice')
21
+ })
22
+
23
+ test('leaves unknown variables intact', () => {
24
+ const result = renderTemplate('Hello {{unknown}}!', {})
25
+ assert.equal(result, 'Hello {{unknown}}!')
26
+ })
27
+
28
+ test('leaves null/undefined variable values intact', () => {
29
+ const result = renderTemplate('{{a}} {{b}}', { a: null, b: undefined })
30
+ assert.equal(result, '{{a}} {{b}}')
31
+ })
32
+
33
+ test('replaces the same variable multiple times', () => {
34
+ const result = renderTemplate('{{x}} and {{x}}', { x: 'foo' })
35
+ assert.equal(result, 'foo and foo')
36
+ })
37
+
38
+ test('handles numeric variable values', () => {
39
+ const result = renderTemplate('Count: {{n}}', { n: 42 })
40
+ assert.equal(result, 'Count: 42')
41
+ })
42
+
43
+ test('no substitution when template has no placeholders', () => {
44
+ const result = renderTemplate('Plain text body.', { anything: 'value' })
45
+ assert.equal(result, 'Plain text body.')
46
+ })
47
+
48
+ test('handles empty template string', () => {
49
+ const result = renderTemplate('', { key: 'val' })
50
+ assert.equal(result, '')
51
+ })
52
+
53
+ test('renders description placeholder used in built-in templates', () => {
54
+ const body = '## Summary\n\n{{description}}\n\n## Details'
55
+ const result = renderTemplate(body, { description: 'A short description.' })
56
+ assert.equal(result, '## Summary\n\nA short description.\n\n## Details')
57
+ })
58
+
59
+ test('multiple standard template variables', () => {
60
+ const body = '{{title}} by {{agent}} on {{date}} in {{repo}}'
61
+ const result = renderTemplate(body, {
62
+ title: 'Fix bug',
63
+ agent: 'bot',
64
+ date: '2025-01-01',
65
+ repo: 'owner/repo',
66
+ })
67
+ assert.equal(result, 'Fix bug by bot on 2025-01-01 in owner/repo')
68
+ })
69
+ })
70
+
71
+ // ---------------------------------------------------------------------------
72
+ // loadTemplate — built-in templates (no file system required for these)
73
+ // ---------------------------------------------------------------------------
74
+
75
+ describe('loadTemplate – built-in templates', () => {
76
+ // Use a temp dir with .git so findRepoRoot() resolves there and no
77
+ // repo-level template files interfere.
78
+ let tmpDir
79
+
80
+ test('loads the default template', () => {
81
+ const tpl = loadTemplate('default', os.tmpdir())
82
+ assert.equal(tpl.key, 'default')
83
+ assert.ok(tpl.body.includes('{{description}}'))
84
+ // source may be 'built-in' or 'user' if the user has a custom default template
85
+ assert.ok(['built-in', 'user'].includes(tpl.source))
86
+ })
87
+
88
+ test('loads the bug template', () => {
89
+ const tpl = loadTemplate('bug', os.tmpdir())
90
+ assert.equal(tpl.key, 'bug')
91
+ assert.ok(tpl.name.length > 0)
92
+ assert.ok(tpl.body.includes('{{description}}'))
93
+ assert.equal(tpl.source, 'built-in')
94
+ })
95
+
96
+ test('loads the feature template', () => {
97
+ const tpl = loadTemplate('feature', os.tmpdir())
98
+ assert.equal(tpl.key, 'feature')
99
+ assert.equal(tpl.source, 'built-in')
100
+ })
101
+
102
+ test('throws for unknown template name', () => {
103
+ assert.throws(
104
+ () => loadTemplate('does-not-exist', os.tmpdir()),
105
+ /Template "does-not-exist" not found/
106
+ )
107
+ })
108
+
109
+ test('returned object has key, name, body, source properties', () => {
110
+ const tpl = loadTemplate('bug', os.tmpdir())
111
+ assert.ok('key' in tpl)
112
+ assert.ok('name' in tpl)
113
+ assert.ok('body' in tpl)
114
+ assert.ok('source' in tpl)
115
+ assert.equal(typeof tpl.body, 'string')
116
+ assert.ok(tpl.body.length > 0)
117
+ })
118
+ })
119
+
120
+ // ---------------------------------------------------------------------------
121
+ // loadTemplate – repo template overrides built-in
122
+ // ---------------------------------------------------------------------------
123
+
124
+ describe('loadTemplate – repo templates override built-in', () => {
125
+ let tmpDir
126
+
127
+ test('repo template shadows built-in when file exists', () => {
128
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tissues-tpl-test-'))
129
+ fs.mkdirSync(path.join(tmpDir, '.git'))
130
+ fs.mkdirSync(path.join(tmpDir, '.tissues', 'templates'), { recursive: true })
131
+ fs.writeFileSync(
132
+ path.join(tmpDir, '.tissues', 'templates', 'bug.md'),
133
+ '## Custom Bug Template\n\n{{description}}\n'
134
+ )
135
+
136
+ const tpl = loadTemplate('bug', tmpDir)
137
+ assert.equal(tpl.source, 'repo')
138
+ assert.ok(tpl.body.includes('Custom Bug Template'))
139
+
140
+ fs.rmSync(tmpDir, { recursive: true, force: true })
141
+ })
142
+
143
+ test('repo template name extracted from file front-matter if present', () => {
144
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tissues-tpl-name-test-'))
145
+ fs.mkdirSync(path.join(tmpDir, '.git'))
146
+ fs.mkdirSync(path.join(tmpDir, '.tissues', 'templates'), { recursive: true })
147
+ fs.writeFileSync(
148
+ path.join(tmpDir, '.tissues', 'templates', 'custom.md'),
149
+ 'name: My Custom Template\n\n## Body\n\n{{description}}\n'
150
+ )
151
+
152
+ const tpl = loadTemplate('custom', tmpDir)
153
+ assert.equal(tpl.source, 'repo')
154
+ assert.equal(tpl.name, 'My Custom Template')
155
+
156
+ fs.rmSync(tmpDir, { recursive: true, force: true })
157
+ })
158
+ })
159
+
160
+ // ---------------------------------------------------------------------------
161
+ // listTemplates
162
+ // ---------------------------------------------------------------------------
163
+
164
+ describe('listTemplates', () => {
165
+ test('returns an array', () => {
166
+ const list = listTemplates(os.tmpdir())
167
+ assert.ok(Array.isArray(list))
168
+ })
169
+
170
+ test('includes all built-in template keys', () => {
171
+ const list = listTemplates(os.tmpdir())
172
+ const builtInKeys = list.filter((t) => t.source === 'built-in').map((t) => t.key)
173
+ for (const key of ['default', 'bug', 'feature']) {
174
+ assert.ok(builtInKeys.includes(key), `missing built-in template: ${key}`)
175
+ }
176
+ })
177
+
178
+ test('each item has key, name, source properties', () => {
179
+ const list = listTemplates(os.tmpdir())
180
+ for (const item of list) {
181
+ assert.ok('key' in item, 'missing key')
182
+ assert.ok('name' in item, 'missing name')
183
+ assert.ok('source' in item, 'missing source')
184
+ }
185
+ })
186
+
187
+ test('repo templates appear before built-ins in the list', () => {
188
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tissues-list-test-'))
189
+ fs.mkdirSync(path.join(tmpDir, '.git'))
190
+ fs.mkdirSync(path.join(tmpDir, '.tissues', 'templates'), { recursive: true })
191
+ fs.writeFileSync(
192
+ path.join(tmpDir, '.tissues', 'templates', 'bug.md'),
193
+ '## Repo Bug\n\n{{description}}\n'
194
+ )
195
+
196
+ try {
197
+ const list = listTemplates(tmpDir)
198
+ const repoIdx = list.findIndex((t) => t.source === 'repo' && t.key === 'bug')
199
+ const builtInIdx = list.findIndex((t) => t.source === 'built-in' && t.key === 'bug')
200
+ assert.ok(repoIdx >= 0, 'repo template not found in list')
201
+ assert.ok(builtInIdx >= 0, 'built-in template not found in list')
202
+ assert.ok(repoIdx < builtInIdx, 'repo template should appear before built-in in list')
203
+ } finally {
204
+ fs.rmSync(tmpDir, { recursive: true, force: true })
205
+ }
206
+ })
207
+ })