blob: abb7b68b88f3527d7b2fde77039d4ccf283949fa [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 { RuntimeHostProtocolError } from '../protocol/errors.js';
import assert from 'node:assert/strict';
import { describe, test } from 'node:test';
import {
decodeClientFrame,
decodeExternalSessionCatalogQueryInput,
decodeExternalSessionCatalogQueryResult,
decodeExternalSessionSourceQueryResult,
EXTERNAL_SESSION_IMPORTED_SESSION_IDS_MAX_ITEMS,
EXTERNAL_SESSION_PAGE_MAX_ITEMS,
HOST_OPERATION_SPECS,
} from '../protocol/index.js';
describe('external Session protocol', () => {
test('identifies external Session queries that expose Host paths', () => {
assert.equal(
HOST_OPERATION_SPECS['external-session.catalog.query'].usesHostPaths?.({
adapterId: 'codex',
workspace: { kind: 'project', projectId: 'project-1' },
}),
false,
);
assert.equal(
HOST_OPERATION_SPECS['external-session.catalog.query'].usesHostPaths?.({
adapterId: 'codex',
workspace: { kind: 'host_path', path: '/workspace' },
}),
true,
);
});
test('round-trips exact source, catalog, and import inputs', () => {
assert.deepEqual(
decodeClientFrame({
requestId: 'request-source',
operation: 'external-session.source.query',
input: {},
}),
{
requestId: 'request-source',
operation: 'external-session.source.query',
input: {},
},
);
assert.deepEqual(
decodeClientFrame({
requestId: 'request-list',
operation: 'external-session.catalog.query',
input: {
adapterId: 'codex',
includeArchived: true,
workspace: { kind: 'project', projectId: 'project-1' },
cursor: '16',
},
}),
{
requestId: 'request-list',
operation: 'external-session.catalog.query',
input: {
adapterId: 'codex',
includeArchived: true,
workspace: { kind: 'project', projectId: 'project-1' },
cursor: '16',
},
},
);
assert.deepEqual(
decodeClientFrame({
requestId: 'request-import',
operation: 'external-session.import',
input: { adapterId: 'codex', sourceSessionId: 'source-session-1' },
}),
{
requestId: 'request-import',
operation: 'external-session.import',
input: { adapterId: 'codex', sourceSessionId: 'source-session-1' },
},
);
});
test('round-trips required external Session import state', () => {
const result = {
sessions: [
{
id: 'source-1',
name: 'Imported source',
hostCwd: '/workspace',
importState: {
importedCount: 10,
importedSessionIds: Array.from(
{ length: EXTERNAL_SESSION_IMPORTED_SESSION_IDS_MAX_ITEMS },
(_, index) => `imported-${EXTERNAL_SESSION_IMPORTED_SESSION_IDS_MAX_ITEMS - index}`,
),
isImporting: true,
},
},
],
nextCursor: null,
};
assert.deepEqual(decodeExternalSessionCatalogQueryResult(result), result);
});
test('rejects missing, open, malformed, and inconsistent external Session import state', () => {
const summary = {
id: 'source-1',
name: 'Imported source',
hostCwd: '/workspace',
};
const decodeState = (importState?: unknown) =>
decodeExternalSessionCatalogQueryResult({
sessions: [{ ...summary, ...(importState === undefined ? {} : { importState }) }],
nextCursor: null,
});
assert.throws(() => decodeState(), isProtocolError);
assert.throws(
() =>
decodeState({
importedCount: 1,
importedSessionIds: ['imported-1'],
isImporting: false,
extra: true,
}),
isProtocolError,
);
assert.throws(
() =>
decodeState({
importedCount: -1,
importedSessionIds: [],
isImporting: false,
}),
isProtocolError,
);
assert.throws(
() =>
decodeState({
importedCount: EXTERNAL_SESSION_IMPORTED_SESSION_IDS_MAX_ITEMS + 1,
importedSessionIds: Array.from(
{ length: EXTERNAL_SESSION_IMPORTED_SESSION_IDS_MAX_ITEMS + 1 },
(_, index) => `imported-${index}`,
),
isImporting: false,
}),
isProtocolError,
);
assert.throws(
() =>
decodeState({
importedCount: 1,
importedSessionIds: ['invalid id'],
isImporting: false,
}),
isProtocolError,
);
assert.throws(
() =>
decodeState({
importedCount: 0,
importedSessionIds: [],
isImporting: 'yes',
}),
isProtocolError,
);
assert.throws(
() =>
decodeState({
importedCount: 1,
importedSessionIds: ['imported-2', 'imported-1'],
isImporting: false,
}),
isProtocolError,
);
});
test('rejects sparse imported Session id arrays', () => {
const importedSessionIds = Array<string>(1);
assert.throws(
() =>
decodeExternalSessionCatalogQueryResult({
sessions: [
{
id: 'source-1',
name: 'Imported source',
hostCwd: '/workspace',
importState: {
importedCount: 1,
importedSessionIds,
isImporting: false,
},
},
],
nextCursor: null,
}),
isProtocolError,
);
});
test('rejects open-ended, malformed, and oversized frames', () => {
assert.throws(
() =>
decodeClientFrame({
requestId: 'request-extra',
operation: 'external-session.source.query',
input: { source: 'codex' },
}),
isProtocolError,
);
assert.throws(
() =>
decodeClientFrame({
requestId: 'request-cursor',
operation: 'external-session.catalog.query',
input: { adapterId: 'codex', cursor: '-1' },
}),
isProtocolError,
);
assert.throws(
() =>
decodeClientFrame({
requestId: 'request-id',
operation: 'external-session.import',
input: { adapterId: 'codex', sourceSessionId: 'bad\nsource' },
}),
isProtocolError,
);
assert.throws(
() =>
decodeExternalSessionCatalogQueryResult({
sessions: Array.from({ length: EXTERNAL_SESSION_PAGE_MAX_ITEMS + 1 }, (_, index) => ({
id: `source-${index}`,
name: `Session ${index}`,
hostCwd: '/workspace',
importState: {
importedCount: 0,
importedSessionIds: [],
isImporting: false,
},
})),
nextCursor: null,
}),
isProtocolError,
);
assert.throws(
() => decodeExternalSessionSourceQueryResult({ adapterIds: ['codex'], extra: true }),
isProtocolError,
);
});
});
function isProtocolError(error: unknown): boolean {
return error instanceof RuntimeHostProtocolError && error.code === 'invalid_frame';
}
describe('external Session catalog query text', () => {
test('a term is accepted and carried through', () => {
assert.deepEqual(
decodeExternalSessionCatalogQueryInput({ adapterId: 'claude-code', text: 'parser' }),
{ adapterId: 'claude-code', text: 'parser' },
);
});
test('an absent term stays absent rather than becoming an empty filter', () => {
assert.deepEqual(decodeExternalSessionCatalogQueryInput({ adapterId: 'codex' }), {
adapterId: 'codex',
});
});
test('a term that is not a bounded string is refused at the frame', () => {
// The term reaches every adapter, so an unbounded or wrong-typed value
// must not get past the protocol edge.
for (const text of [42, null, {}, '', 'x'.repeat(1_000)]) {
assert.throws(
() => decodeExternalSessionCatalogQueryInput({ adapterId: 'codex', text }),
RuntimeHostProtocolError,
`text=${JSON.stringify(text)?.slice(0, 24)}`,
);
}
});
test('an unknown field is still refused', () => {
assert.throws(
() => decodeExternalSessionCatalogQueryInput({ adapterId: 'codex', query: 'parser' }),
RuntimeHostProtocolError,
);
});
});