blob: 435db9750073c893760ba56806baaf15f7fe19ce [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import assert from 'node:assert/strict';
import { describe, test } from 'node:test';
import {
EXTERNAL_SESSION_QUERY_TEXT_MAX_CHARS,
externalSessionMatchesQuery,
normalizeExternalSessionQueryText,
sameExternalSessionPath,
type ExternalSessionSummary,
} from '../external-session.js';
function summary(over: Partial<ExternalSessionSummary> = {}): ExternalSessionSummary {
return { id: 'sess-1', name: 'Fix the parser', cwd: '/Users/z/maka-agent', ...over };
}
describe('externalSessionMatchesQuery', () => {
test('an absent term selects everything', () => {
assert.equal(externalSessionMatchesQuery(summary()), true);
assert.equal(externalSessionMatchesQuery(summary(), {}), true);
});
test('a term matches the title, case-insensitively', () => {
assert.equal(externalSessionMatchesQuery(summary(), { text: 'parser' }), true);
assert.equal(externalSessionMatchesQuery(summary(), { text: 'PARSER' }), true);
assert.equal(externalSessionMatchesQuery(summary(), { text: 'Fix the' }), true);
});
test('a term matches the project path', () => {
// The other half of "the session I half remember": people navigate by
// where the work happened as often as by what it was called.
assert.equal(externalSessionMatchesQuery(summary(), { text: 'maka-agent' }), true);
assert.equal(externalSessionMatchesQuery(summary(), { text: '/Users/z' }), true);
});
test('a term matches the source Session id', () => {
assert.equal(externalSessionMatchesQuery(summary(), { text: 'SESS-1' }), true);
assert.equal(
externalSessionMatchesQuery(summary({ id: '01JY7W3FKJZKQ2G7A2P9V4M8NE' }), {
text: 'q2g7a2p9',
}),
true,
);
});
test('a term that matches neither excludes the row', () => {
assert.equal(externalSessionMatchesQuery(summary(), { text: 'kubernetes' }), false);
});
test('a blank box is not a filter', () => {
// A stray space must not hide every session. `''` and `' '` both mean
// "no term", which is why the term is normalized rather than truthiness-
// checked at each call site.
for (const text of ['', ' ', '\t\n']) {
assert.equal(externalSessionMatchesQuery(summary(), { text }), true, JSON.stringify(text));
}
assert.equal(normalizeExternalSessionQueryText(' '), undefined);
assert.equal(normalizeExternalSessionQueryText(undefined), undefined);
assert.equal(normalizeExternalSessionQueryText(' Parser '), 'parser');
});
test('an over-long term is truncated rather than refused', () => {
// Bounded because the term reaches every adapter. Truncating keeps a long
// paste working on its prefix instead of throwing at the user.
const term = 'a'.repeat(EXTERNAL_SESSION_QUERY_TEXT_MAX_CHARS + 50);
const normalized = normalizeExternalSessionQueryText(term);
assert.equal(normalized?.length, EXTERNAL_SESSION_QUERY_TEXT_MAX_CHARS);
});
test('a term pasted from a Windows path finds a forward-slash summary', () => {
// `sameExternalSessionPath` already calls these the same project. Before
// the shared normalizer, the text match disagreed with the cwd filter
// about the same two strings.
const win = summary({ cwd: 'C:/Repo/App' });
assert.equal(
externalSessionMatchesQuery(win, {
text: 'C:' + String.fromCharCode(92) + 'Repo' + String.fromCharCode(92) + 'App',
}),
true,
);
assert.equal(externalSessionMatchesQuery(win, { text: 'c:/repo/app' }), true);
// A summary recorded with backslashes is reachable from either spelling.
const stored = summary({
cwd: 'C:' + String.fromCharCode(92) + 'Repo' + String.fromCharCode(92) + 'App',
});
assert.equal(externalSessionMatchesQuery(stored, { text: 'C:/Repo/App' }), true);
});
test('separator folding stays on the path pair and off the title', () => {
// Folding a title would make `/n` match a title holding a literal
// backslash-n; folding the term but not the title would stop the term
// that names it from finding it. Both directions are checked because a
// one-sided fold trades one bug for the other.
const backslash = String.fromCharCode(92);
const title = summary({ name: `regex: ${backslash}n handling`, cwd: '/repo' });
assert.equal(externalSessionMatchesQuery(title, { text: `${backslash}n` }), true);
assert.equal(externalSessionMatchesQuery(title, { text: '/n' }), false);
// The path pair still folds, which is what makes a pasted Windows path work.
const win = summary({ name: 'untitled', cwd: 'C:/Repo/App' });
assert.equal(
externalSessionMatchesQuery(win, { text: `C:${backslash}Repo${backslash}App` }),
true,
);
});
test('composed and decomposed Unicode name the same conversation', () => {
// macOS records decomposed filenames, so the same visible title arrives
// in NFC or NFD depending on where it was typed.
const nfc = 'caf\u00e9-notes';
const nfd = 'cafe\u0301-notes';
assert.notEqual(nfc, nfd, 'fixture must actually differ by normalization form');
assert.equal(externalSessionMatchesQuery(summary({ name: nfc }), { text: nfd }), true);
assert.equal(externalSessionMatchesQuery(summary({ name: nfd }), { text: nfc }), true);
assert.equal(sameExternalSessionPath('/repo/' + nfc, '/repo/' + nfd), true);
});
test('archived rows stay hidden unless asked for, term or no term', () => {
const archived = summary({ archived: true });
assert.equal(externalSessionMatchesQuery(archived, {}), false);
assert.equal(externalSessionMatchesQuery(archived, { text: 'parser' }), false);
assert.equal(externalSessionMatchesQuery(archived, { includeArchived: true }), true);
assert.equal(
externalSessionMatchesQuery(archived, { includeArchived: true, text: 'parser' }),
true,
);
});
test('cwd and text both apply, not either', () => {
const query = { cwd: '/Users/z/maka-agent', text: 'parser' };
assert.equal(externalSessionMatchesQuery(summary(), query), true);
assert.equal(externalSessionMatchesQuery(summary({ cwd: '/elsewhere' }), query), false);
assert.equal(externalSessionMatchesQuery(summary({ name: 'unrelated' }), query), false);
});
test('a term matching one field is enough', () => {
assert.equal(
externalSessionMatchesQuery(summary({ name: 'untitled' }), { text: 'maka-agent' }),
true,
);
assert.equal(externalSessionMatchesQuery(summary({ cwd: '/tmp' }), { text: 'parser' }), true);
});
});
describe('sameExternalSessionPath', () => {
test('separators and a trailing slash do not change the project', () => {
// The Claude Code adapter compared raw strings, so the same project
// reached through a different separator answered "no such project".
assert.equal(sameExternalSessionPath('C:/Repo/App', 'C:\\Repo\\App'), true);
assert.equal(sameExternalSessionPath('/repo/app/', '/repo/app'), true);
assert.equal(sameExternalSessionPath('/repo/app', '/repo/app'), true);
});
test('a Windows drive letter is case-insensitive, a POSIX path is not', () => {
assert.equal(sameExternalSessionPath('C:/Repo', 'c:/repo'), true);
// Two different directories on a case-sensitive filesystem.
assert.equal(sameExternalSessionPath('/Repo', '/repo'), false);
});
test('the POSIX root is not the same as an unknown cwd', () => {
// Stripping trailing separators unconditionally folded `/` and `''` to the
// same value, so a workspace at filesystem root matched every session
// whose cwd the adapter could not determine. The root IS its separator.
assert.equal(sameExternalSessionPath('/', ''), false);
assert.equal(sameExternalSessionPath('/', '/'), true);
assert.equal(sameExternalSessionPath('//', '/'), true);
assert.equal(sameExternalSessionPath('', ''), true);
assert.equal(sameExternalSessionPath('/', '/repo'), false);
});
test('a term with a trailing separator still names the project', () => {
// Windows Explorer copies `C:\\Repo\\App\\`, trailing backslash included.
const backslash = String.fromCharCode(92);
const win = summary({ name: 'untitled', cwd: 'C:/Repo/App' });
assert.equal(
externalSessionMatchesQuery(win, {
text: `C:${backslash}Repo${backslash}App${backslash}`,
}),
true,
);
assert.equal(
externalSessionMatchesQuery(summary({ cwd: '/repo/app' }), { text: '/repo/app/' }),
true,
);
});
test('different projects stay different', () => {
assert.equal(sameExternalSessionPath('/repo/app', '/repo/other'), false);
assert.equal(sameExternalSessionPath('/repo/app', '/repo/app-2'), false);
});
});