@dalzoubi/dev-agents-sync 2.0.24 → 2.0.26

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.
@@ -1,313 +0,0 @@
1
- /**
2
- * tests/rules-injector-runtime.test.mjs
3
- *
4
- * Slice S2 — SessionStart rules-injector.sh hook (runtime behavior half).
5
- * Spec: docs/specs/260708-claude-rules-enforcement/
6
- *
7
- * Pins the runtime behavior of the shipped `.claude/hooks/rules-injector.sh`
8
- * script (mirrored from the canonical `.agents/hooks/rules-injector.sh`, which
9
- * does not exist yet — these tests are the RED anchor for that build step).
10
- * Follows the steering-injector.sh convention: the hook is invoked with the
11
- * consumer repo root as its cwd and looks for its inputs relative to that cwd,
12
- * so we exercise it against an isolated tmp fixture dir rather than depending
13
- * on this repo's real `.claude/rules/` contents (which may change).
14
- *
15
- * Behavior anchors:
16
- *
17
- * T1. Executing the script against a fixture dir containing
18
- * `.claude/rules/structured-choices.md` and `.claude/rules/tone.md`
19
- * emits both rule BODIES to stdout, with the YAML frontmatter and the
20
- * `<!-- managed-by: ... -->` marker line stripped.
21
- * T2. The script exits 0 on that happy path.
22
- * T3. When one rule file is absent, the script still exits 0 (fail-safe),
23
- * emits at most a stderr note, and still surfaces the present rule's body.
24
- * T4. When both rule files are absent (no .claude/rules/ dir at all), the
25
- * script still exits 0.
26
- *
27
- * Test runner: `node --test packages/dev-agents-sync/tests/rules-injector-runtime.test.mjs`
28
- * No third-party dependencies. All tests use isolated tmp dirs; none of them
29
- * depend on this repo's actual `.claude/rules/*.md` content.
30
- */
31
-
32
- import { describe, it, beforeEach, afterEach } from 'node:test';
33
- import assert from 'node:assert/strict';
34
- import { spawnSync } from 'node:child_process';
35
- import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
36
- import { tmpdir } from 'node:os';
37
- import path from 'node:path';
38
- import { fileURLToPath } from 'node:url';
39
-
40
- // ---------------------------------------------------------------------------
41
- // Paths
42
- // ---------------------------------------------------------------------------
43
-
44
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
45
- // packages/dev-agents-sync/tests -> packages/dev-agents-sync -> packages -> repo root
46
- const REPO_ROOT = path.resolve(__dirname, '..', '..', '..');
47
- const SCRIPT_PATH = path.join(REPO_ROOT, '.claude', 'hooks', 'rules-injector.sh');
48
-
49
- // ---------------------------------------------------------------------------
50
- // Fixture content — self-contained, does NOT depend on this repo's actual
51
- // .claude/rules/*.md bodies so the test stays stable if those rules are edited.
52
- // ---------------------------------------------------------------------------
53
-
54
- const STRUCTURED_CHOICES_BODY_MARKER = 'FIXTURE_STRUCTURED_CHOICES_BODY_9f3a1';
55
- const TONE_BODY_MARKER = 'FIXTURE_TONE_BODY_7c22e';
56
- const FRONTMATTER_NAME_MARKER = 'name: "structured-choices"';
57
- const MARKDOWN_MANAGED_MARKER = '<!-- managed-by: dev-agents-sync v0.1.0 -->';
58
-
59
- function structuredChoicesFixture() {
60
- return [
61
- '---',
62
- 'name: "structured-choices"',
63
- 'description: "fixture rule for rules-injector runtime tests"',
64
- '---',
65
- MARKDOWN_MANAGED_MARKER,
66
- '# Structured choices',
67
- '',
68
- STRUCTURED_CHOICES_BODY_MARKER,
69
- '',
70
- ].join('\n');
71
- }
72
-
73
- function toneFixture() {
74
- return [
75
- '---',
76
- 'name: "tone"',
77
- 'description: "fixture rule for rules-injector runtime tests"',
78
- '---',
79
- MARKDOWN_MANAGED_MARKER,
80
- '# Tone',
81
- '',
82
- TONE_BODY_MARKER,
83
- '',
84
- ].join('\n');
85
- }
86
-
87
- function makeTmpDir() {
88
- return mkdtempSync(path.join(tmpdir(), 'das-rules-injector-runtime-test-'));
89
- }
90
-
91
- /** Runs the shipped rules-injector.sh with `cwd` as its working directory. */
92
- function runScript(cwd) {
93
- return spawnSync('sh', [SCRIPT_PATH], { cwd, encoding: 'utf8' });
94
- }
95
-
96
- let fixtureDir;
97
-
98
- // ---------------------------------------------------------------------------
99
- // T1/T2 — happy path: both rules present
100
- // ---------------------------------------------------------------------------
101
-
102
- describe('S2 T1/T2 — rules-injector.sh emits both rule bodies, exits 0', () => {
103
- beforeEach(() => {
104
- fixtureDir = makeTmpDir();
105
- const rulesDir = path.join(fixtureDir, '.claude', 'rules');
106
- mkdirSync(rulesDir, { recursive: true });
107
- writeFileSync(path.join(rulesDir, 'structured-choices.md'), structuredChoicesFixture(), 'utf8');
108
- writeFileSync(path.join(rulesDir, 'tone.md'), toneFixture(), 'utf8');
109
- });
110
-
111
- afterEach(() => {
112
- rmSync(fixtureDir, { recursive: true, force: true });
113
- });
114
-
115
- it('T2: exits 0 when both rule files are present', () => {
116
- const result = runScript(fixtureDir);
117
- assert.equal(
118
- result.status,
119
- 0,
120
- `rules-injector.sh must exit 0 on the happy path; got status=${result.status}, stderr=${result.stderr}, error=${result.error}`,
121
- );
122
- });
123
-
124
- it('T1a: stdout includes the structured-choices.md body', () => {
125
- const result = runScript(fixtureDir);
126
- assert.ok(
127
- (result.stdout ?? '').includes(STRUCTURED_CHOICES_BODY_MARKER),
128
- `stdout must include the structured-choices.md body; got stdout=${JSON.stringify(result.stdout)}`,
129
- );
130
- });
131
-
132
- it('T1b: stdout includes the tone.md body', () => {
133
- const result = runScript(fixtureDir);
134
- assert.ok(
135
- (result.stdout ?? '').includes(TONE_BODY_MARKER),
136
- `stdout must include the tone.md body; got stdout=${JSON.stringify(result.stdout)}`,
137
- );
138
- });
139
-
140
- it('T1c: stdout strips the YAML frontmatter (no "name:" front-matter key leaks through)', () => {
141
- const result = runScript(fixtureDir);
142
- const stdout = result.stdout ?? '';
143
- // Anchored to the body actually being emitted — otherwise "frontmatter is
144
- // absent" would pass vacuously against empty stdout (e.g. the script not
145
- // existing yet) without proving anything about stripping behavior.
146
- assert.ok(
147
- stdout.includes(STRUCTURED_CHOICES_BODY_MARKER),
148
- `precondition failed: stdout must carry the rule body before frontmatter-stripping can be checked; got stdout=${JSON.stringify(stdout)}`,
149
- );
150
- assert.ok(
151
- !stdout.includes(FRONTMATTER_NAME_MARKER),
152
- `stdout must not include raw frontmatter (${FRONTMATTER_NAME_MARKER}); got stdout=${JSON.stringify(stdout)}`,
153
- );
154
- });
155
-
156
- it('T1d: stdout strips the managed-by marker line', () => {
157
- const result = runScript(fixtureDir);
158
- const stdout = result.stdout ?? '';
159
- // Same anchoring as T1c: require the body to be present so this cannot
160
- // pass vacuously against empty output.
161
- assert.ok(
162
- stdout.includes(TONE_BODY_MARKER),
163
- `precondition failed: stdout must carry the rule body before marker-stripping can be checked; got stdout=${JSON.stringify(stdout)}`,
164
- );
165
- assert.ok(
166
- !stdout.includes(MARKDOWN_MANAGED_MARKER),
167
- `stdout must not include the raw managed-by marker line; got stdout=${JSON.stringify(stdout)}`,
168
- );
169
- });
170
- });
171
-
172
- // ---------------------------------------------------------------------------
173
- // T3 — fail-safe: one rule file absent
174
- // ---------------------------------------------------------------------------
175
-
176
- describe('S2 T3 — rules-injector.sh is fail-safe when one rule file is absent', () => {
177
- beforeEach(() => {
178
- fixtureDir = makeTmpDir();
179
- const rulesDir = path.join(fixtureDir, '.claude', 'rules');
180
- mkdirSync(rulesDir, { recursive: true });
181
- // Only tone.md exists; structured-choices.md is missing.
182
- writeFileSync(path.join(rulesDir, 'tone.md'), toneFixture(), 'utf8');
183
- });
184
-
185
- afterEach(() => {
186
- rmSync(fixtureDir, { recursive: true, force: true });
187
- });
188
-
189
- it('T3a: still exits 0 when structured-choices.md is absent', () => {
190
- const result = runScript(fixtureDir);
191
- assert.equal(
192
- result.status,
193
- 0,
194
- `rules-injector.sh must exit 0 even when a rule file is absent (fail-safe); got status=${result.status}, stderr=${result.stderr}, error=${result.error}`,
195
- );
196
- });
197
-
198
- it('T3b: still surfaces the present rule (tone.md) body on stdout', () => {
199
- const result = runScript(fixtureDir);
200
- assert.ok(
201
- (result.stdout ?? '').includes(TONE_BODY_MARKER),
202
- `stdout must still include the present tone.md body; got stdout=${JSON.stringify(result.stdout)}`,
203
- );
204
- });
205
-
206
- it('T3c: emits at most a stderr note about the missing rule (does not crash silently on stdout)', () => {
207
- const result = runScript(fixtureDir);
208
- const stdout = result.stdout ?? '';
209
- // Anchored to exit 0 + the present rule's body actually being emitted —
210
- // otherwise "no fabricated content" would pass vacuously (e.g. against
211
- // empty stdout from a script that doesn't exist yet or crashed outright)
212
- // without proving the fail-safe skip behavior.
213
- assert.equal(
214
- result.status,
215
- 0,
216
- `precondition failed: script must exit 0 before the fail-safe skip behavior can be checked; got status=${result.status}`,
217
- );
218
- assert.ok(
219
- stdout.includes(TONE_BODY_MARKER),
220
- `precondition failed: stdout must carry the present rule's body before checking the missing rule was skipped cleanly; got stdout=${JSON.stringify(stdout)}`,
221
- );
222
- // "at most a stderr note" — we don't require stderr, but if the script
223
- // emits anything about the skip, it must go to stderr, not stdout, and
224
- // must not fabricate content for the missing rule on stdout.
225
- assert.ok(
226
- !stdout.includes('structured-choices'),
227
- `stdout must not fabricate content for the missing structured-choices.md; got stdout=${JSON.stringify(stdout)}`,
228
- );
229
- });
230
- });
231
-
232
- // ---------------------------------------------------------------------------
233
- // T5 — CRLF robustness: frontmatter fences with a trailing CR are still stripped
234
- // (regression anchor for M1 — the awk fence match must tolerate `---\r`, else
235
- // on a CRLF checkout the whole YAML frontmatter leaks into injected context).
236
- // ---------------------------------------------------------------------------
237
-
238
- describe('S2 T5 — rules-injector.sh strips frontmatter on a CRLF checkout', () => {
239
- beforeEach(() => {
240
- fixtureDir = makeTmpDir();
241
- const rulesDir = path.join(fixtureDir, '.claude', 'rules');
242
- mkdirSync(rulesDir, { recursive: true });
243
- // Re-encode both fixtures with CRLF line endings, as a CRLF checkout would.
244
- const toCRLF = (s) => s.replace(/\n/g, '\r\n');
245
- writeFileSync(path.join(rulesDir, 'structured-choices.md'), toCRLF(structuredChoicesFixture()), 'utf8');
246
- writeFileSync(path.join(rulesDir, 'tone.md'), toCRLF(toneFixture()), 'utf8');
247
- });
248
-
249
- afterEach(() => {
250
- rmSync(fixtureDir, { recursive: true, force: true });
251
- });
252
-
253
- it('T5a: exits 0 and still emits the rule bodies on a CRLF checkout', () => {
254
- const result = runScript(fixtureDir);
255
- assert.equal(
256
- result.status,
257
- 0,
258
- `rules-injector.sh must exit 0 on a CRLF checkout; got status=${result.status}, stderr=${result.stderr}, error=${result.error}`,
259
- );
260
- assert.ok(
261
- (result.stdout ?? '').includes(STRUCTURED_CHOICES_BODY_MARKER),
262
- `stdout must still include the structured-choices.md body on a CRLF checkout; got stdout=${JSON.stringify(result.stdout)}`,
263
- );
264
- });
265
-
266
- it('T5b: strips the CRLF frontmatter (no "name:" key and no bare "---" fence leaks)', () => {
267
- const result = runScript(fixtureDir);
268
- const stdout = result.stdout ?? '';
269
- // Anchor to the body actually being emitted, so this cannot pass vacuously.
270
- assert.ok(
271
- stdout.includes(STRUCTURED_CHOICES_BODY_MARKER),
272
- `precondition failed: stdout must carry the rule body before frontmatter-stripping can be checked; got stdout=${JSON.stringify(stdout)}`,
273
- );
274
- assert.ok(
275
- !stdout.includes(FRONTMATTER_NAME_MARKER),
276
- `stdout must not leak raw frontmatter (${FRONTMATTER_NAME_MARKER}) on a CRLF checkout; got stdout=${JSON.stringify(stdout)}`,
277
- );
278
- // No line should be a bare YAML fence (with or without a trailing CR).
279
- assert.ok(
280
- !/^---\r?$/m.test(stdout),
281
- `stdout must not leak a bare "---" frontmatter fence on a CRLF checkout; got stdout=${JSON.stringify(stdout)}`,
282
- );
283
- // The managed-by marker must also be gone even with the trailing CR.
284
- assert.ok(
285
- !stdout.includes(MARKDOWN_MANAGED_MARKER),
286
- `stdout must not leak the managed-by marker line on a CRLF checkout; got stdout=${JSON.stringify(stdout)}`,
287
- );
288
- });
289
- });
290
-
291
- // ---------------------------------------------------------------------------
292
- // T4 — fail-safe: no .claude/rules/ directory at all
293
- // ---------------------------------------------------------------------------
294
-
295
- describe('S2 T4 — rules-injector.sh is fail-safe when no rule files exist', () => {
296
- beforeEach(() => {
297
- fixtureDir = makeTmpDir();
298
- // Deliberately do NOT create .claude/rules/ at all.
299
- });
300
-
301
- afterEach(() => {
302
- rmSync(fixtureDir, { recursive: true, force: true });
303
- });
304
-
305
- it('T4: exits 0 when .claude/rules/ does not exist', () => {
306
- const result = runScript(fixtureDir);
307
- assert.equal(
308
- result.status,
309
- 0,
310
- `rules-injector.sh must exit 0 when .claude/rules/ is entirely absent; got status=${result.status}, stderr=${result.stderr}, error=${result.error}`,
311
- );
312
- });
313
- });