@applica-software-guru/sdd-core 1.7.0 → 1.8.1
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/dist/remote/api-client.d.ts +7 -1
- package/dist/remote/api-client.d.ts.map +1 -1
- package/dist/remote/api-client.js +15 -0
- package/dist/remote/api-client.js.map +1 -1
- package/dist/remote/sync-engine.d.ts.map +1 -1
- package/dist/remote/sync-engine.js +153 -30
- package/dist/remote/sync-engine.js.map +1 -1
- package/dist/remote/types.d.ts +9 -0
- package/dist/remote/types.d.ts.map +1 -1
- package/dist/sdd.js +1 -1
- package/dist/sdd.js.map +1 -1
- package/package.json +1 -1
- package/src/remote/api-client.ts +25 -0
- package/src/remote/sync-engine.ts +168 -32
- package/src/remote/types.ts +10 -0
- package/src/sdd.ts +1 -1
- package/tests/apply.test.ts +12 -14
- package/tests/bug.test.ts +22 -0
- package/tests/integration.test.ts +1 -1
- package/tests/prompt.test.ts +3 -5
- package/tests/remote-state.test.ts +2 -2
- package/tests/sync-engine.test.ts +156 -5
|
@@ -4,6 +4,7 @@ import { existsSync } from 'node:fs';
|
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
6
|
import { execSync } from 'node:child_process';
|
|
7
|
+
import { createHash } from 'node:crypto';
|
|
7
8
|
import { SDD } from '../src/sdd.js';
|
|
8
9
|
import { readRemoteState } from '../src/remote/state.js';
|
|
9
10
|
import type { RemoteDocResponse, RemoteDocBulkResponse, RemoteCRResponse, RemoteBugResponse } from '../src/remote/types.js';
|
|
@@ -12,6 +13,10 @@ function git(cmd: string, cwd: string): string {
|
|
|
12
13
|
return execSync(`git ${cmd}`, { cwd, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
13
14
|
}
|
|
14
15
|
|
|
16
|
+
function sha256(content: string): string {
|
|
17
|
+
return createHash('sha256').update(content, 'utf-8').digest('hex');
|
|
18
|
+
}
|
|
19
|
+
|
|
15
20
|
const VISION_MD = `---
|
|
16
21
|
title: "Product Vision"
|
|
17
22
|
status: new
|
|
@@ -43,6 +48,67 @@ function makeDocResponse(overrides: Partial<RemoteDocResponse> = {}): RemoteDocR
|
|
|
43
48
|
};
|
|
44
49
|
}
|
|
45
50
|
|
|
51
|
+
function makeCRResponse(overrides: Partial<RemoteCRResponse> = {}): RemoteCRResponse {
|
|
52
|
+
return {
|
|
53
|
+
id: 'cr-001',
|
|
54
|
+
project_id: 'proj-001',
|
|
55
|
+
path: 'change-requests/CR-001.md',
|
|
56
|
+
title: 'Add auth flow',
|
|
57
|
+
body: '## Description\n\nAdd JWT authentication.\n',
|
|
58
|
+
status: 'pending',
|
|
59
|
+
author_id: 'user-1',
|
|
60
|
+
assignee_id: null,
|
|
61
|
+
target_files: null,
|
|
62
|
+
closed_at: null,
|
|
63
|
+
created_at: '2026-01-01T00:00:00.000Z',
|
|
64
|
+
updated_at: '2026-01-01T00:00:00.000Z',
|
|
65
|
+
...overrides,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function makeBugResponse(overrides: Partial<RemoteBugResponse> = {}): RemoteBugResponse {
|
|
70
|
+
return {
|
|
71
|
+
id: 'bug-001',
|
|
72
|
+
project_id: 'proj-001',
|
|
73
|
+
path: 'bugs/BUG-001.md',
|
|
74
|
+
title: 'Search returns stale results',
|
|
75
|
+
body: '## Steps\n\n1. Search for a term\n2. Results are outdated\n',
|
|
76
|
+
status: 'open',
|
|
77
|
+
severity: 'major',
|
|
78
|
+
author_id: 'user-1',
|
|
79
|
+
assignee_id: null,
|
|
80
|
+
closed_at: null,
|
|
81
|
+
created_at: '2026-02-01T00:00:00.000Z',
|
|
82
|
+
updated_at: '2026-02-01T00:00:00.000Z',
|
|
83
|
+
...overrides,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const APPLIED_CR = `---
|
|
88
|
+
title: "Add auth flow"
|
|
89
|
+
status: applied
|
|
90
|
+
author: "user"
|
|
91
|
+
created-at: "2026-01-01T00:00:00.000Z"
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## Description
|
|
95
|
+
|
|
96
|
+
Add JWT authentication.
|
|
97
|
+
`;
|
|
98
|
+
|
|
99
|
+
const RESOLVED_BUG = `---
|
|
100
|
+
title: "Search returns stale results"
|
|
101
|
+
status: resolved
|
|
102
|
+
author: "user"
|
|
103
|
+
created-at: "2026-02-01T00:00:00.000Z"
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Steps
|
|
107
|
+
|
|
108
|
+
1. Search for a term
|
|
109
|
+
2. Results are outdated
|
|
110
|
+
`;
|
|
111
|
+
|
|
46
112
|
describe('Sync engine - push', () => {
|
|
47
113
|
let tempDir: string;
|
|
48
114
|
let sdd: SDD;
|
|
@@ -67,7 +133,7 @@ describe('Sync engine - push', () => {
|
|
|
67
133
|
await rm(tempDir, { recursive: true });
|
|
68
134
|
});
|
|
69
135
|
|
|
70
|
-
it('pushes pending files and
|
|
136
|
+
it('pushes pending files and preserves local frontmatter status', async () => {
|
|
71
137
|
await writeFile(join(tempDir, 'product/vision.md'), VISION_MD, 'utf-8');
|
|
72
138
|
|
|
73
139
|
const pushResponse: RemoteDocBulkResponse = {
|
|
@@ -87,9 +153,9 @@ describe('Sync engine - push', () => {
|
|
|
87
153
|
expect(result.updated).toBe(0);
|
|
88
154
|
expect(result.pushed).toContain('product/vision.md');
|
|
89
155
|
|
|
90
|
-
//
|
|
156
|
+
// Push should not mutate markdown frontmatter status
|
|
91
157
|
const content = await readFile(join(tempDir, 'product/vision.md'), 'utf-8');
|
|
92
|
-
expect(content).toContain('status:
|
|
158
|
+
expect(content).toContain('status: new');
|
|
93
159
|
|
|
94
160
|
// Remote state should be updated
|
|
95
161
|
const state = await readRemoteState(tempDir);
|
|
@@ -101,6 +167,20 @@ describe('Sync engine - push', () => {
|
|
|
101
167
|
it('skips synced files when no paths specified', async () => {
|
|
102
168
|
await writeFile(join(tempDir, 'product/vision.md'), VISION_SYNCED, 'utf-8');
|
|
103
169
|
|
|
170
|
+
const { writeRemoteState } = await import('../src/remote/state.js');
|
|
171
|
+
await writeRemoteState(tempDir, {
|
|
172
|
+
documents: {
|
|
173
|
+
'product/vision.md': {
|
|
174
|
+
remoteId: 'doc-001',
|
|
175
|
+
remoteVersion: 1,
|
|
176
|
+
localHash: sha256(VISION_SYNCED),
|
|
177
|
+
lastSynced: '2026-01-01T00:00:00.000Z',
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
changeRequests: {},
|
|
181
|
+
bugs: {},
|
|
182
|
+
});
|
|
183
|
+
|
|
104
184
|
const result = await sdd.push();
|
|
105
185
|
expect(result.pushed).toHaveLength(0);
|
|
106
186
|
});
|
|
@@ -123,6 +203,77 @@ describe('Sync engine - push', () => {
|
|
|
123
203
|
expect(result.pushed).toContain('product/vision.md');
|
|
124
204
|
expect(result.updated).toBe(1);
|
|
125
205
|
});
|
|
206
|
+
|
|
207
|
+
it('repopulates remote from local files after a remote reset', async () => {
|
|
208
|
+
await writeFile(join(tempDir, 'product/vision.md'), VISION_SYNCED, 'utf-8');
|
|
209
|
+
await mkdir(join(tempDir, 'change-requests'), { recursive: true });
|
|
210
|
+
await writeFile(join(tempDir, 'change-requests/CR-001.md'), APPLIED_CR, 'utf-8');
|
|
211
|
+
await mkdir(join(tempDir, 'bugs'), { recursive: true });
|
|
212
|
+
await writeFile(join(tempDir, 'bugs/BUG-001.md'), RESOLVED_BUG, 'utf-8');
|
|
213
|
+
|
|
214
|
+
globalThis.fetch = vi.fn()
|
|
215
|
+
.mockResolvedValueOnce({
|
|
216
|
+
ok: true,
|
|
217
|
+
status: 200,
|
|
218
|
+
json: async () => ({
|
|
219
|
+
message: 'Remote project reset.',
|
|
220
|
+
deleted_documents: 4,
|
|
221
|
+
deleted_change_requests: 2,
|
|
222
|
+
deleted_bugs: 1,
|
|
223
|
+
deleted_comments: 0,
|
|
224
|
+
deleted_notifications: 0,
|
|
225
|
+
}),
|
|
226
|
+
})
|
|
227
|
+
.mockResolvedValueOnce({
|
|
228
|
+
ok: true,
|
|
229
|
+
status: 200,
|
|
230
|
+
json: async () => ({
|
|
231
|
+
created: 1,
|
|
232
|
+
updated: 0,
|
|
233
|
+
documents: [makeDocResponse()],
|
|
234
|
+
}),
|
|
235
|
+
})
|
|
236
|
+
.mockResolvedValueOnce({
|
|
237
|
+
ok: true,
|
|
238
|
+
status: 200,
|
|
239
|
+
json: async () => ({
|
|
240
|
+
created: 1,
|
|
241
|
+
updated: 0,
|
|
242
|
+
change_requests: [makeCRResponse()],
|
|
243
|
+
}),
|
|
244
|
+
})
|
|
245
|
+
.mockResolvedValueOnce({
|
|
246
|
+
ok: true,
|
|
247
|
+
status: 200,
|
|
248
|
+
json: async () => ({
|
|
249
|
+
created: 1,
|
|
250
|
+
updated: 0,
|
|
251
|
+
bugs: [makeBugResponse()],
|
|
252
|
+
}),
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
await sdd.remoteReset('test-project');
|
|
256
|
+
|
|
257
|
+
const resetState = await readRemoteState(tempDir);
|
|
258
|
+
expect(resetState.needsReseed).toBe(true);
|
|
259
|
+
expect(resetState.documents).toEqual({});
|
|
260
|
+
expect(resetState.changeRequests).toEqual({});
|
|
261
|
+
expect(resetState.bugs).toEqual({});
|
|
262
|
+
|
|
263
|
+
const result = await sdd.push();
|
|
264
|
+
|
|
265
|
+
expect(result.created).toBe(3);
|
|
266
|
+
expect(result.updated).toBe(0);
|
|
267
|
+
expect(result.pushed).toContain('product/vision.md');
|
|
268
|
+
expect(result.pushed).toContain('change-requests/CR-001.md');
|
|
269
|
+
expect(result.pushed).toContain('bugs/BUG-001.md');
|
|
270
|
+
|
|
271
|
+
const state = await readRemoteState(tempDir);
|
|
272
|
+
expect(state.needsReseed).toBe(false);
|
|
273
|
+
expect(state.documents['product/vision.md']?.remoteId).toBe('doc-001');
|
|
274
|
+
expect(state.changeRequests?.['change-requests/CR-001.md']?.remoteId).toBe('cr-001');
|
|
275
|
+
expect(state.bugs?.['bugs/BUG-001.md']?.remoteId).toBe('bug-001');
|
|
276
|
+
});
|
|
126
277
|
});
|
|
127
278
|
|
|
128
279
|
describe('Sync engine - pull', () => {
|
|
@@ -505,9 +656,9 @@ Easypick is a marketplace for on-demand services.
|
|
|
505
656
|
});
|
|
506
657
|
await sdd.push();
|
|
507
658
|
|
|
508
|
-
// Read file after push (status
|
|
659
|
+
// Read file after push (status is preserved)
|
|
509
660
|
const afterPush = await readFile(join(tempDir, 'product/vision.md'), 'utf-8');
|
|
510
|
-
expect(afterPush).toContain('status:
|
|
661
|
+
expect(afterPush).toContain('status: new');
|
|
511
662
|
|
|
512
663
|
// Pull — server returns same content, higher version
|
|
513
664
|
globalThis.fetch = vi.fn().mockResolvedValue({
|