@bongos/core 1.19.380 → 1.19.382

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.
@@ -112,27 +112,144 @@ test('snippetOf flattens whitespace, caps length, and scrubs', () => {
112
112
  });
113
113
 
114
114
  // ---------- resolveTranscriptPath (#551: worktree sessions) ----------
115
+ //
116
+ // FIXTURE NOTE: the expected transcript path is DERIVED via encodeProjectPath(
117
+ // path.resolve(dir)) rather than hard-coded as its POSIX encoding. path.resolve
118
+ // is platform-dependent — on win32 '/proj' resolves to 'C:\\\\proj', which encodes
119
+ // to 'C--proj', not '-proj' — so hard-coded encodings made these two tests fail
120
+ // on Windows while passing in Linux CI. Deriving them asserts the same behaviour
121
+ // on both. (POSIX encoding itself is pinned separately below.)
122
+ const sidPath = (base, dir, sid) =>
123
+ path.join(base, 'projects', encodeProjectPath(path.resolve(dir)), `${sid}.jsonl`);
115
124
 
116
125
  test('resolveTranscriptPath prefers the session CWD (worktree) over REPO_ROOT', () => {
117
126
  const sid = 'abc-123';
118
127
  const base = '/fake/.claude';
119
128
  const worktree = '/proj/.claude/worktrees/wt1';
120
129
  const mainRepo = '/proj';
121
- const wtPath = path.join(base, 'projects', '-proj--claude-worktrees-wt1', `${sid}.jsonl`);
130
+ const wtPath = sidPath(base, worktree, sid);
122
131
  // Only the worktree-encoded path exists — the bug looked at REPO_ROOT first.
123
- const got = resolveTranscriptPath(sid, [worktree, mainRepo], { base, existsFn: (p) => p === wtPath });
132
+ const got = resolveTranscriptPath(sid, [worktree, mainRepo], {
133
+ base,
134
+ existsFn: (p) => p === wtPath,
135
+ readdirFn: () => [],
136
+ });
124
137
  assert.equal(got, wtPath);
125
138
  });
126
139
 
127
140
  test('resolveTranscriptPath falls back to REPO_ROOT, else null', () => {
128
141
  const sid = 'abc-123';
129
142
  const base = '/fake/.claude';
130
- const mainPath = path.join(base, 'projects', '-proj', `${sid}.jsonl`);
143
+ const mainPath = sidPath(base, '/proj', sid);
131
144
  assert.equal(
132
- resolveTranscriptPath(sid, ['/proj/.claude/worktrees/wt1', '/proj'], { base, existsFn: (p) => p === mainPath }),
145
+ resolveTranscriptPath(sid, ['/proj/.claude/worktrees/wt1', '/proj'], {
146
+ base,
147
+ existsFn: (p) => p === mainPath,
148
+ readdirFn: () => [],
149
+ }),
133
150
  mainPath
134
151
  );
135
- assert.equal(resolveTranscriptPath(sid, ['/a', '/b'], { base, existsFn: () => false }), null);
152
+ assert.equal(
153
+ resolveTranscriptPath(sid, ['/a', '/b'], { base, existsFn: () => false, readdirFn: () => [] }),
154
+ null
155
+ );
156
+ });
157
+
158
+ // ---------- resolveTranscriptPath scan fallback (task 1002505) ----------
159
+ //
160
+ // The two fast paths above NARROW the miss, they do not seal it: the session
161
+ // driving a ship need not be rooted in process.cwd() OR REPO_ROOT. A session
162
+ // rooted in ANOTHER repo can file a task here and ship it (proven on task
163
+ // 1002475), and before this fallback both probes missed, classifyUploadSkip
164
+ // returned transcript_not_found, no session_record was written, and the
165
+ // cost-plus reward — the only stream this instance pays (ADR 0146) — never
166
+ // booked. The fallback scans every dir under <base>/projects for
167
+ // <sessionId>.jsonl; a session id is a UUID, so a filename hit is unambiguous.
168
+
169
+ test('resolveTranscriptPath finds a transcript rooted in a DIFFERENT project dir (the 1002475 shape)', () => {
170
+ const sid = 'e53f445b-308a-454e-9ac1-9c22d0188a37';
171
+ const base = '/fake/.claude';
172
+ // The orchestrating session lived under an unrelated repo's encoded dir, so
173
+ // neither the claim worktree nor REPO_ROOT could ever match it.
174
+ const foreign = path.join(base, 'projects', 'C--Users-x-Git-repos-other-project', `${sid}.jsonl`);
175
+ const got = resolveTranscriptPath(sid, ['/proj/.claude/worktrees/wt1', '/proj'], {
176
+ base,
177
+ existsFn: (p) => p === foreign,
178
+ readdirFn: () => ['C--Users-x-Git-repos-other-project', '-proj', '-proj--claude-worktrees-wt1'],
179
+ });
180
+ assert.equal(got, foreign);
181
+ });
182
+
183
+ test('resolveTranscriptPath scan only matches the exact session id — no mis-attribution', () => {
184
+ const sid = 'abc-123';
185
+ const base = '/fake/.claude';
186
+ const other = path.join(base, 'projects', '-somewhere-else', 'def-456.jsonl');
187
+ assert.equal(
188
+ resolveTranscriptPath(sid, ['/proj'], {
189
+ base,
190
+ existsFn: (p) => p === other,
191
+ readdirFn: () => ['-somewhere-else'],
192
+ }),
193
+ null
194
+ );
195
+ });
196
+
197
+ test('resolveTranscriptPath does not scan when a fast path hits (precedence unchanged)', () => {
198
+ const sid = 'abc-123';
199
+ const base = '/fake/.claude';
200
+ const wtPath = sidPath(base, '/proj/.claude/worktrees/wt1', sid);
201
+ let scanned = 0;
202
+ const got = resolveTranscriptPath(sid, ['/proj/.claude/worktrees/wt1', '/proj'], {
203
+ base,
204
+ existsFn: (p) => p === wtPath,
205
+ readdirFn: () => { scanned++; return []; },
206
+ });
207
+ assert.equal(got, wtPath);
208
+ assert.equal(scanned, 0, 'the scan must not run once a fast path has matched');
209
+ });
210
+
211
+ test('resolveTranscriptPath fills opts.tried: the fast paths, plus ONE scan summary', () => {
212
+ const sid = 'abc-123';
213
+ const base = '/fake/.claude';
214
+ const tried = [];
215
+ const got = resolveTranscriptPath(sid, ['/proj/.claude/worktrees/wt1', '/proj'], {
216
+ base,
217
+ existsFn: () => false,
218
+ readdirFn: () => ['-a', '-b', '-c'],
219
+ tried,
220
+ });
221
+ assert.equal(got, null);
222
+ // Two exact fast paths...
223
+ assert.equal(tried[0], sidPath(base, '/proj/.claude/worktrees/wt1', sid));
224
+ assert.equal(tried[1], sidPath(base, '/proj', sid));
225
+ // ...then a single bounded summary, not one line per scanned dir, so the
226
+ // transcript_not_found reason stays readable however many projects exist.
227
+ assert.equal(tried.length, 3);
228
+ assert.ok(tried[2].includes('scanned 3 project dirs'), tried[2]);
229
+ });
230
+
231
+ test('resolveTranscriptPath survives an unreadable projects dir and says so', () => {
232
+ const sid = 'abc-123';
233
+ const base = '/fake/.claude';
234
+ const tried = [];
235
+ const got = resolveTranscriptPath(sid, ['/proj'], {
236
+ base,
237
+ existsFn: () => false,
238
+ readdirFn: () => { throw new Error('ENOENT'); },
239
+ tried,
240
+ });
241
+ assert.equal(got, null, 'a missing/unreadable projects dir must not throw into the ship path');
242
+ assert.ok(tried[tried.length - 1].includes('could not read the projects dir'), tried.join(' ; '));
243
+ });
244
+
245
+ test('resolveTranscriptPath tolerates a null/absent projectDirs list', () => {
246
+ const sid = 'abc-123';
247
+ const base = '/fake/.claude';
248
+ const hit = path.join(base, 'projects', '-only-dir', `${sid}.jsonl`);
249
+ assert.equal(
250
+ resolveTranscriptPath(sid, null, { base, existsFn: (p) => p === hit, readdirFn: () => ['-only-dir'] }),
251
+ hit
252
+ );
136
253
  });
137
254
 
138
255
  // ---------- encodeProjectPath (task 1002471: win32 separators) ----------