@dalzoubi/dev-agents-sync 1.0.15 → 1.0.16
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/package.json +1 -1
- package/src/commands/check.mjs +2 -1
- package/src/commands/init.mjs +20 -9
- package/src/commands/update.mjs +17 -6
- package/src/marker.mjs +10 -0
- package/src/writer.mjs +14 -5
- package/tests/check-constitution-version.test.mjs +594 -0
- package/tests/first-sync-notice.test.mjs +459 -0
- package/tests/fixtures/release-v1.0.0/root/CONSTITUTION.md +117 -0
- package/tests/root-target.test.mjs +440 -0
|
@@ -0,0 +1,594 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tests/check-constitution-version.test.mjs
|
|
3
|
+
*
|
|
4
|
+
* Tests for scripts/check-constitution-version.mjs — the PR-CI script that
|
|
5
|
+
* enforces the "Constitution version: vN" bump rule on prompts/constitution.md.
|
|
6
|
+
*
|
|
7
|
+
* The script does NOT exist yet (version-bump-check-script slice is pending).
|
|
8
|
+
* All tests here are expected to fail with "Cannot find" / non-zero exit until
|
|
9
|
+
* the implement agent creates the script.
|
|
10
|
+
*
|
|
11
|
+
* Approach: real temporary git repos — no mocking of git. Each test:
|
|
12
|
+
* 1. Creates a tmpdir and initialises a bare git repo in it.
|
|
13
|
+
* 2. Makes one or two commits representing "base" and "HEAD" states.
|
|
14
|
+
* 3. Spawns `node <scriptPath> --base <mergeBase>` as a subprocess.
|
|
15
|
+
* 4. Asserts exit code and (where applicable) stderr content.
|
|
16
|
+
*
|
|
17
|
+
* Node built-in test runner (node:test) — no jest/mocha/vitest.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { describe, it, before, after } from 'node:test';
|
|
21
|
+
import assert from 'node:assert/strict';
|
|
22
|
+
import { execSync, spawnSync } from 'node:child_process';
|
|
23
|
+
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
|
|
24
|
+
import { join, resolve } from 'node:path';
|
|
25
|
+
import { tmpdir } from 'node:os';
|
|
26
|
+
import { fileURLToPath } from 'node:url';
|
|
27
|
+
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// Resolve script path — absolute, relative to this test file's location.
|
|
30
|
+
// The script lives at <repo-root>/scripts/check-constitution-version.mjs.
|
|
31
|
+
// This test file lives at packages/dev-agents-sync/tests/
|
|
32
|
+
// so ../../.. reaches the repo root.
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
const TEST_FILE_DIR = fileURLToPath(new URL('.', import.meta.url));
|
|
36
|
+
const SCRIPT_PATH = resolve(TEST_FILE_DIR, '..', '..', '..', 'scripts', 'check-constitution-version.mjs');
|
|
37
|
+
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
// Git helpers — all operate inside a provided repoDir.
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Run a git command synchronously inside repoDir; throw on non-zero.
|
|
44
|
+
* @param {string} repoDir
|
|
45
|
+
* @param {string[]} args
|
|
46
|
+
* @returns {string} trimmed stdout
|
|
47
|
+
*/
|
|
48
|
+
function git(repoDir, args) {
|
|
49
|
+
const result = spawnSync('git', args, {
|
|
50
|
+
cwd: repoDir,
|
|
51
|
+
encoding: 'utf8',
|
|
52
|
+
env: {
|
|
53
|
+
...process.env,
|
|
54
|
+
// Ensure a consistent git identity so tests work in CI.
|
|
55
|
+
GIT_AUTHOR_NAME: 'Test',
|
|
56
|
+
GIT_AUTHOR_EMAIL: 'test@test.invalid',
|
|
57
|
+
GIT_COMMITTER_NAME: 'Test',
|
|
58
|
+
GIT_COMMITTER_EMAIL: 'test@test.invalid',
|
|
59
|
+
GIT_CONFIG_NOSYSTEM: '1',
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
if (result.status !== 0) {
|
|
63
|
+
throw new Error(
|
|
64
|
+
`git ${args.join(' ')} failed (exit ${result.status}): ${result.stderr}`,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
return (result.stdout || '').trimEnd();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Initialise a new git repo in a temp directory and make an initial (empty) commit
|
|
72
|
+
* so the repo has a valid HEAD.
|
|
73
|
+
* @returns {{ repoDir: string, cleanup: () => void }}
|
|
74
|
+
*/
|
|
75
|
+
function createTempRepo() {
|
|
76
|
+
const repoDir = mkdtempSync(join(tmpdir(), 'const-ver-test-'));
|
|
77
|
+
git(repoDir, ['init', '-b', 'main']);
|
|
78
|
+
git(repoDir, ['config', 'user.email', 'test@test.invalid']);
|
|
79
|
+
git(repoDir, ['config', 'user.name', 'Test']);
|
|
80
|
+
|
|
81
|
+
// Initial commit so the repo is not empty.
|
|
82
|
+
writeFileSync(join(repoDir, 'README.md'), 'placeholder\n');
|
|
83
|
+
git(repoDir, ['add', 'README.md']);
|
|
84
|
+
git(repoDir, ['commit', '-m', 'initial']);
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
repoDir,
|
|
88
|
+
cleanup: () => rmSync(repoDir, { recursive: true, force: true }),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Write prompts/constitution.md into a repo, stage and commit it.
|
|
94
|
+
* @param {string} repoDir
|
|
95
|
+
* @param {string} content — full file content
|
|
96
|
+
* @param {string} message — commit message
|
|
97
|
+
* @returns {string} the SHA of the new commit
|
|
98
|
+
*/
|
|
99
|
+
function commitConstitution(repoDir, content, message) {
|
|
100
|
+
const promptsDir = join(repoDir, 'prompts');
|
|
101
|
+
mkdirSync(promptsDir, { recursive: true });
|
|
102
|
+
writeFileSync(join(promptsDir, 'constitution.md'), content);
|
|
103
|
+
git(repoDir, ['add', join('prompts', 'constitution.md')]);
|
|
104
|
+
git(repoDir, ['commit', '-m', message]);
|
|
105
|
+
return git(repoDir, ['rev-parse', 'HEAD']);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Resolve the merge-base SHA of HEAD against the given baseRef.
|
|
110
|
+
* @param {string} repoDir
|
|
111
|
+
* @param {string} baseRef — a commit SHA or ref
|
|
112
|
+
* @returns {string} merge-base SHA
|
|
113
|
+
*/
|
|
114
|
+
function mergeBase(repoDir, baseRef) {
|
|
115
|
+
return git(repoDir, ['merge-base', 'HEAD', baseRef]);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
// Canonical constitution content builders.
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
|
|
122
|
+
const BASE_CONTENT_V1 = `# Constitution
|
|
123
|
+
|
|
124
|
+
Constitution version: v1
|
|
125
|
+
|
|
126
|
+
## Principles
|
|
127
|
+
|
|
128
|
+
§1 Be good.
|
|
129
|
+
§2 Be consistent.
|
|
130
|
+
`;
|
|
131
|
+
|
|
132
|
+
const BASE_CONTENT_V2_BUMPED = `# Constitution
|
|
133
|
+
|
|
134
|
+
Constitution version: v2
|
|
135
|
+
|
|
136
|
+
## Principles
|
|
137
|
+
|
|
138
|
+
§1 Be good.
|
|
139
|
+
§2 Be consistent.
|
|
140
|
+
§3 Be kind.
|
|
141
|
+
`;
|
|
142
|
+
|
|
143
|
+
const BASE_CONTENT_V2_UNBUMPED = `# Constitution
|
|
144
|
+
|
|
145
|
+
Constitution version: v1
|
|
146
|
+
|
|
147
|
+
## Principles
|
|
148
|
+
|
|
149
|
+
§1 Be good.
|
|
150
|
+
§2 Be consistent.
|
|
151
|
+
§3 Be kind.
|
|
152
|
+
`;
|
|
153
|
+
|
|
154
|
+
const BASE_CONTENT_V0_DECREASED = `# Constitution
|
|
155
|
+
|
|
156
|
+
Constitution version: v0
|
|
157
|
+
|
|
158
|
+
## Principles
|
|
159
|
+
|
|
160
|
+
§1 Be good.
|
|
161
|
+
§2 Be consistent.
|
|
162
|
+
§3 Be kind.
|
|
163
|
+
`;
|
|
164
|
+
|
|
165
|
+
const BASE_CONTENT_VERSION_LINE_ONLY_CHANGE = `# Constitution
|
|
166
|
+
|
|
167
|
+
Constitution version: v2
|
|
168
|
+
|
|
169
|
+
## Principles
|
|
170
|
+
|
|
171
|
+
§1 Be good.
|
|
172
|
+
§2 Be consistent.
|
|
173
|
+
`;
|
|
174
|
+
|
|
175
|
+
const BASE_CONTENT_MISSING_VERSION_LINE = `# Constitution
|
|
176
|
+
|
|
177
|
+
## Principles
|
|
178
|
+
|
|
179
|
+
§1 Be good.
|
|
180
|
+
§2 Be consistent.
|
|
181
|
+
`;
|
|
182
|
+
|
|
183
|
+
const BASE_CONTENT_MALFORMED_VERSION_LINE = `# Constitution
|
|
184
|
+
|
|
185
|
+
Constitution version: banana
|
|
186
|
+
|
|
187
|
+
## Principles
|
|
188
|
+
|
|
189
|
+
§1 Be good.
|
|
190
|
+
`;
|
|
191
|
+
|
|
192
|
+
// ---------------------------------------------------------------------------
|
|
193
|
+
// Invoke the script as a subprocess.
|
|
194
|
+
// Returns { status, stdout, stderr }.
|
|
195
|
+
// ---------------------------------------------------------------------------
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Run the check script synchronously.
|
|
199
|
+
* @param {string} repoDir — cwd for the invocation
|
|
200
|
+
* @param {string[]} extraArgs — passed after `node <scriptPath>`
|
|
201
|
+
* @param {Record<string,string>} [extraEnv] — merged into process.env
|
|
202
|
+
* @returns {{ status: number, stdout: string, stderr: string }}
|
|
203
|
+
*/
|
|
204
|
+
function runScript(repoDir, extraArgs = [], extraEnv = {}) {
|
|
205
|
+
const result = spawnSync('node', [SCRIPT_PATH, ...extraArgs], {
|
|
206
|
+
cwd: repoDir,
|
|
207
|
+
encoding: 'utf8',
|
|
208
|
+
env: {
|
|
209
|
+
...process.env,
|
|
210
|
+
// Ensure GITHUB_BASE_REF is not accidentally inherited from CI.
|
|
211
|
+
GITHUB_BASE_REF: undefined,
|
|
212
|
+
GIT_AUTHOR_NAME: 'Test',
|
|
213
|
+
GIT_AUTHOR_EMAIL: 'test@test.invalid',
|
|
214
|
+
GIT_COMMITTER_NAME: 'Test',
|
|
215
|
+
GIT_COMMITTER_EMAIL: 'test@test.invalid',
|
|
216
|
+
GIT_CONFIG_NOSYSTEM: '1',
|
|
217
|
+
...extraEnv,
|
|
218
|
+
},
|
|
219
|
+
});
|
|
220
|
+
return {
|
|
221
|
+
status: result.status ?? 1,
|
|
222
|
+
stdout: result.stdout || '',
|
|
223
|
+
stderr: result.stderr || '',
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ---------------------------------------------------------------------------
|
|
228
|
+
// Test suite
|
|
229
|
+
// ---------------------------------------------------------------------------
|
|
230
|
+
|
|
231
|
+
describe('check-constitution-version.mjs', () => {
|
|
232
|
+
// -------------------------------------------------------------------------
|
|
233
|
+
// Case 1: Pass — no body change (file byte-identical at base and HEAD)
|
|
234
|
+
// -------------------------------------------------------------------------
|
|
235
|
+
it('Pass — no body change: file byte-identical → exit 0', () => {
|
|
236
|
+
const { repoDir, cleanup } = createTempRepo();
|
|
237
|
+
try {
|
|
238
|
+
// base commit
|
|
239
|
+
const baseCommit = commitConstitution(
|
|
240
|
+
repoDir,
|
|
241
|
+
BASE_CONTENT_V1,
|
|
242
|
+
'add constitution',
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
// HEAD is the same commit (nothing changed).
|
|
246
|
+
const base = mergeBase(repoDir, baseCommit);
|
|
247
|
+
const result = runScript(repoDir, ['--base', base]);
|
|
248
|
+
|
|
249
|
+
assert.equal(
|
|
250
|
+
result.status,
|
|
251
|
+
0,
|
|
252
|
+
`expected exit 0, got ${result.status}. stderr: ${result.stderr}`,
|
|
253
|
+
);
|
|
254
|
+
} finally {
|
|
255
|
+
cleanup();
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
// -------------------------------------------------------------------------
|
|
260
|
+
// Case 2: Pass — body change with proper bump (non-version line differs, integer increased)
|
|
261
|
+
// -------------------------------------------------------------------------
|
|
262
|
+
it('Pass — body change with proper bump: non-version line differs and integer increased → exit 0', () => {
|
|
263
|
+
const { repoDir, cleanup } = createTempRepo();
|
|
264
|
+
try {
|
|
265
|
+
// base commit (v1)
|
|
266
|
+
const baseCommit = commitConstitution(
|
|
267
|
+
repoDir,
|
|
268
|
+
BASE_CONTENT_V1,
|
|
269
|
+
'add constitution v1',
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
// HEAD commit (v2, with a new principle)
|
|
273
|
+
commitConstitution(
|
|
274
|
+
repoDir,
|
|
275
|
+
BASE_CONTENT_V2_BUMPED,
|
|
276
|
+
'bump constitution to v2',
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
const base = mergeBase(repoDir, baseCommit);
|
|
280
|
+
const result = runScript(repoDir, ['--base', base]);
|
|
281
|
+
|
|
282
|
+
assert.equal(
|
|
283
|
+
result.status,
|
|
284
|
+
0,
|
|
285
|
+
`expected exit 0, got ${result.status}. stderr: ${result.stderr}`,
|
|
286
|
+
);
|
|
287
|
+
} finally {
|
|
288
|
+
cleanup();
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
// -------------------------------------------------------------------------
|
|
293
|
+
// Case 3: Pass — version-line-only change (only version line differs)
|
|
294
|
+
// -------------------------------------------------------------------------
|
|
295
|
+
it('Pass — version-line-only change: only the version line differs → exit 0', () => {
|
|
296
|
+
const { repoDir, cleanup } = createTempRepo();
|
|
297
|
+
try {
|
|
298
|
+
// base commit (v1)
|
|
299
|
+
const baseCommit = commitConstitution(
|
|
300
|
+
repoDir,
|
|
301
|
+
BASE_CONTENT_V1,
|
|
302
|
+
'add constitution v1',
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
// HEAD commit: same body but version bumped to v2, no other change.
|
|
306
|
+
commitConstitution(
|
|
307
|
+
repoDir,
|
|
308
|
+
BASE_CONTENT_VERSION_LINE_ONLY_CHANGE,
|
|
309
|
+
'pre-bump constitution version to v2',
|
|
310
|
+
);
|
|
311
|
+
|
|
312
|
+
const base = mergeBase(repoDir, baseCommit);
|
|
313
|
+
const result = runScript(repoDir, ['--base', base]);
|
|
314
|
+
|
|
315
|
+
assert.equal(
|
|
316
|
+
result.status,
|
|
317
|
+
0,
|
|
318
|
+
`expected exit 0, got ${result.status}. stderr: ${result.stderr}`,
|
|
319
|
+
);
|
|
320
|
+
} finally {
|
|
321
|
+
cleanup();
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
// -------------------------------------------------------------------------
|
|
326
|
+
// Case 4: Pass — new file (did not exist at merge base, HEAD has parseable vN ≥ 1)
|
|
327
|
+
// -------------------------------------------------------------------------
|
|
328
|
+
it('Pass — new file (no base copy): HEAD has parseable vN line → exit 0', () => {
|
|
329
|
+
const { repoDir, cleanup } = createTempRepo();
|
|
330
|
+
try {
|
|
331
|
+
// Capture the initial commit SHA before any constitution exists.
|
|
332
|
+
const beforeConstitution = git(repoDir, ['rev-parse', 'HEAD']);
|
|
333
|
+
|
|
334
|
+
// Now add the constitution at HEAD.
|
|
335
|
+
commitConstitution(
|
|
336
|
+
repoDir,
|
|
337
|
+
BASE_CONTENT_V1,
|
|
338
|
+
'introduce constitution',
|
|
339
|
+
);
|
|
340
|
+
|
|
341
|
+
// Merge base is the commit before the constitution was added.
|
|
342
|
+
const base = mergeBase(repoDir, beforeConstitution);
|
|
343
|
+
const result = runScript(repoDir, ['--base', base]);
|
|
344
|
+
|
|
345
|
+
assert.equal(
|
|
346
|
+
result.status,
|
|
347
|
+
0,
|
|
348
|
+
`expected exit 0, got ${result.status}. stderr: ${result.stderr}`,
|
|
349
|
+
);
|
|
350
|
+
} finally {
|
|
351
|
+
cleanup();
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
// -------------------------------------------------------------------------
|
|
356
|
+
// Case 5: Fail — body change without bump (non-version line differs, version same)
|
|
357
|
+
// -------------------------------------------------------------------------
|
|
358
|
+
it('Fail — body change without bump: version stayed same → exit 1 with "bump from vN to vN+1" message', () => {
|
|
359
|
+
const { repoDir, cleanup } = createTempRepo();
|
|
360
|
+
try {
|
|
361
|
+
// base commit (v1)
|
|
362
|
+
const baseCommit = commitConstitution(
|
|
363
|
+
repoDir,
|
|
364
|
+
BASE_CONTENT_V1,
|
|
365
|
+
'add constitution v1',
|
|
366
|
+
);
|
|
367
|
+
|
|
368
|
+
// HEAD commit: body changed but version NOT bumped.
|
|
369
|
+
commitConstitution(
|
|
370
|
+
repoDir,
|
|
371
|
+
BASE_CONTENT_V2_UNBUMPED,
|
|
372
|
+
'add new principle (forgot to bump version)',
|
|
373
|
+
);
|
|
374
|
+
|
|
375
|
+
const base = mergeBase(repoDir, baseCommit);
|
|
376
|
+
const result = runScript(repoDir, ['--base', base]);
|
|
377
|
+
|
|
378
|
+
assert.equal(
|
|
379
|
+
result.status,
|
|
380
|
+
1,
|
|
381
|
+
`expected exit 1, got ${result.status}. stderr: ${result.stderr}`,
|
|
382
|
+
);
|
|
383
|
+
assert.ok(
|
|
384
|
+
result.stderr.includes('bump from v1 to v2'),
|
|
385
|
+
`expected "bump from v1 to v2" in stderr, got: ${result.stderr}`,
|
|
386
|
+
);
|
|
387
|
+
} finally {
|
|
388
|
+
cleanup();
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
// -------------------------------------------------------------------------
|
|
393
|
+
// Case 6: Fail — body change with version decrease (integer decreased)
|
|
394
|
+
// -------------------------------------------------------------------------
|
|
395
|
+
it('Fail — body change with version decrease: integer decreased → exit 1 with bump message', () => {
|
|
396
|
+
const { repoDir, cleanup } = createTempRepo();
|
|
397
|
+
try {
|
|
398
|
+
// base commit (v1)
|
|
399
|
+
const baseCommit = commitConstitution(
|
|
400
|
+
repoDir,
|
|
401
|
+
BASE_CONTENT_V1,
|
|
402
|
+
'add constitution v1',
|
|
403
|
+
);
|
|
404
|
+
|
|
405
|
+
// HEAD commit: body changed AND version decreased to v0.
|
|
406
|
+
commitConstitution(
|
|
407
|
+
repoDir,
|
|
408
|
+
BASE_CONTENT_V0_DECREASED,
|
|
409
|
+
'accidentally decreased version',
|
|
410
|
+
);
|
|
411
|
+
|
|
412
|
+
const base = mergeBase(repoDir, baseCommit);
|
|
413
|
+
const result = runScript(repoDir, ['--base', base]);
|
|
414
|
+
|
|
415
|
+
assert.equal(
|
|
416
|
+
result.status,
|
|
417
|
+
1,
|
|
418
|
+
`expected exit 1, got ${result.status}. stderr: ${result.stderr}`,
|
|
419
|
+
);
|
|
420
|
+
// The decrease case should produce a "bump from vN to vN+1" message because
|
|
421
|
+
// the version did not increase — the specific values reference the base
|
|
422
|
+
// side (v1) and the required next value (v2).
|
|
423
|
+
assert.ok(
|
|
424
|
+
result.stderr.includes('bump from'),
|
|
425
|
+
`expected "bump from" in stderr for version-decrease case, got: ${result.stderr}`,
|
|
426
|
+
);
|
|
427
|
+
} finally {
|
|
428
|
+
cleanup();
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
// -------------------------------------------------------------------------
|
|
433
|
+
// Case 7a: Fail — missing version line at HEAD
|
|
434
|
+
// -------------------------------------------------------------------------
|
|
435
|
+
it('Fail — missing version line at HEAD: absent → exit 1 with "missing or malformed" message', () => {
|
|
436
|
+
const { repoDir, cleanup } = createTempRepo();
|
|
437
|
+
try {
|
|
438
|
+
// base commit (valid v1)
|
|
439
|
+
const baseCommit = commitConstitution(
|
|
440
|
+
repoDir,
|
|
441
|
+
BASE_CONTENT_V1,
|
|
442
|
+
'add constitution v1',
|
|
443
|
+
);
|
|
444
|
+
|
|
445
|
+
// HEAD commit: version line removed.
|
|
446
|
+
commitConstitution(
|
|
447
|
+
repoDir,
|
|
448
|
+
BASE_CONTENT_MISSING_VERSION_LINE,
|
|
449
|
+
'accidentally removed version line',
|
|
450
|
+
);
|
|
451
|
+
|
|
452
|
+
const base = mergeBase(repoDir, baseCommit);
|
|
453
|
+
const result = runScript(repoDir, ['--base', base]);
|
|
454
|
+
|
|
455
|
+
assert.equal(
|
|
456
|
+
result.status,
|
|
457
|
+
1,
|
|
458
|
+
`expected exit 1, got ${result.status}. stderr: ${result.stderr}`,
|
|
459
|
+
);
|
|
460
|
+
assert.ok(
|
|
461
|
+
result.stderr.includes('missing or malformed'),
|
|
462
|
+
`expected "missing or malformed" in stderr, got: ${result.stderr}`,
|
|
463
|
+
);
|
|
464
|
+
} finally {
|
|
465
|
+
cleanup();
|
|
466
|
+
}
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
// -------------------------------------------------------------------------
|
|
470
|
+
// Case 7b: Fail — malformed version line at HEAD
|
|
471
|
+
// -------------------------------------------------------------------------
|
|
472
|
+
it('Fail — malformed version line at HEAD: non-integer value → exit 1 with "missing or malformed" message', () => {
|
|
473
|
+
const { repoDir, cleanup } = createTempRepo();
|
|
474
|
+
try {
|
|
475
|
+
// base commit (valid v1)
|
|
476
|
+
const baseCommit = commitConstitution(
|
|
477
|
+
repoDir,
|
|
478
|
+
BASE_CONTENT_V1,
|
|
479
|
+
'add constitution v1',
|
|
480
|
+
);
|
|
481
|
+
|
|
482
|
+
// HEAD commit: version line present but malformed.
|
|
483
|
+
commitConstitution(
|
|
484
|
+
repoDir,
|
|
485
|
+
BASE_CONTENT_MALFORMED_VERSION_LINE,
|
|
486
|
+
'accidentally malformed version line',
|
|
487
|
+
);
|
|
488
|
+
|
|
489
|
+
const base = mergeBase(repoDir, baseCommit);
|
|
490
|
+
const result = runScript(repoDir, ['--base', base]);
|
|
491
|
+
|
|
492
|
+
assert.equal(
|
|
493
|
+
result.status,
|
|
494
|
+
1,
|
|
495
|
+
`expected exit 1, got ${result.status}. stderr: ${result.stderr}`,
|
|
496
|
+
);
|
|
497
|
+
assert.ok(
|
|
498
|
+
result.stderr.includes('missing or malformed'),
|
|
499
|
+
`expected "missing or malformed" in stderr, got: ${result.stderr}`,
|
|
500
|
+
);
|
|
501
|
+
} finally {
|
|
502
|
+
cleanup();
|
|
503
|
+
}
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
// -------------------------------------------------------------------------
|
|
507
|
+
// Case 8: Fail — missing base ref (neither GITHUB_BASE_REF nor --base set)
|
|
508
|
+
// -------------------------------------------------------------------------
|
|
509
|
+
it('Fail — missing base ref: neither GITHUB_BASE_REF nor --base → exit 1 with actionable message', () => {
|
|
510
|
+
const { repoDir, cleanup } = createTempRepo();
|
|
511
|
+
try {
|
|
512
|
+
commitConstitution(repoDir, BASE_CONTENT_V1, 'add constitution');
|
|
513
|
+
|
|
514
|
+
// Run with no --base and GITHUB_BASE_REF explicitly unset.
|
|
515
|
+
const result = spawnSync('node', [SCRIPT_PATH], {
|
|
516
|
+
cwd: repoDir,
|
|
517
|
+
encoding: 'utf8',
|
|
518
|
+
env: {
|
|
519
|
+
...process.env,
|
|
520
|
+
GITHUB_BASE_REF: undefined,
|
|
521
|
+
GIT_AUTHOR_NAME: 'Test',
|
|
522
|
+
GIT_AUTHOR_EMAIL: 'test@test.invalid',
|
|
523
|
+
GIT_COMMITTER_NAME: 'Test',
|
|
524
|
+
GIT_COMMITTER_EMAIL: 'test@test.invalid',
|
|
525
|
+
GIT_CONFIG_NOSYSTEM: '1',
|
|
526
|
+
},
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
assert.equal(
|
|
530
|
+
(result.status ?? 1),
|
|
531
|
+
1,
|
|
532
|
+
`expected exit 1, got ${result.status}. stderr: ${result.stderr}`,
|
|
533
|
+
);
|
|
534
|
+
|
|
535
|
+
const combined = (result.stderr || '') + (result.stdout || '');
|
|
536
|
+
|
|
537
|
+
// The error message must name both inputs so the user knows how to fix it.
|
|
538
|
+
assert.ok(
|
|
539
|
+
combined.includes('GITHUB_BASE_REF'),
|
|
540
|
+
`expected "GITHUB_BASE_REF" in output, got: ${combined}`,
|
|
541
|
+
);
|
|
542
|
+
assert.ok(
|
|
543
|
+
combined.includes('--base'),
|
|
544
|
+
`expected "--base" in output, got: ${combined}`,
|
|
545
|
+
);
|
|
546
|
+
} finally {
|
|
547
|
+
cleanup();
|
|
548
|
+
}
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
// -------------------------------------------------------------------------
|
|
552
|
+
// Case 9: Determinism — same inputs → identical exit code and output both runs
|
|
553
|
+
// -------------------------------------------------------------------------
|
|
554
|
+
it('Determinism — same inputs produce identical exit code and output on two runs', () => {
|
|
555
|
+
const { repoDir, cleanup } = createTempRepo();
|
|
556
|
+
try {
|
|
557
|
+
// Set up a "fail" scenario (body changed, version not bumped) so both
|
|
558
|
+
// stderr output and exit code are non-trivial and comparable.
|
|
559
|
+
const baseCommit = commitConstitution(
|
|
560
|
+
repoDir,
|
|
561
|
+
BASE_CONTENT_V1,
|
|
562
|
+
'add constitution v1',
|
|
563
|
+
);
|
|
564
|
+
commitConstitution(
|
|
565
|
+
repoDir,
|
|
566
|
+
BASE_CONTENT_V2_UNBUMPED,
|
|
567
|
+
'add new principle (forgot to bump version)',
|
|
568
|
+
);
|
|
569
|
+
|
|
570
|
+
const base = mergeBase(repoDir, baseCommit);
|
|
571
|
+
|
|
572
|
+
const run1 = runScript(repoDir, ['--base', base]);
|
|
573
|
+
const run2 = runScript(repoDir, ['--base', base]);
|
|
574
|
+
|
|
575
|
+
assert.equal(
|
|
576
|
+
run1.status,
|
|
577
|
+
run2.status,
|
|
578
|
+
`exit codes differ: run1=${run1.status} run2=${run2.status}`,
|
|
579
|
+
);
|
|
580
|
+
assert.equal(
|
|
581
|
+
run1.stderr,
|
|
582
|
+
run2.stderr,
|
|
583
|
+
`stderr differs between runs:\nrun1: ${run1.stderr}\nrun2: ${run2.stderr}`,
|
|
584
|
+
);
|
|
585
|
+
assert.equal(
|
|
586
|
+
run1.stdout,
|
|
587
|
+
run2.stdout,
|
|
588
|
+
`stdout differs between runs:\nrun1: ${run1.stdout}\nrun2: ${run2.stdout}`,
|
|
589
|
+
);
|
|
590
|
+
} finally {
|
|
591
|
+
cleanup();
|
|
592
|
+
}
|
|
593
|
+
});
|
|
594
|
+
});
|