Merge pull request 'smoke: reconcile assertions with ungated-global + ambient clock' (#140) from fix/smoke-ungated-global-reconcile into main
Some checks failed
Some checks failed
This commit was merged in pull request #140.
This commit is contained in:
@@ -155,7 +155,7 @@ describe('Smoke: Audit events', () => {
|
||||
try {
|
||||
await chat.initialize();
|
||||
const tools = await chat.listTools();
|
||||
chat.check('Gated (only begin_session)', tools.length, (v) => v >= 1);
|
||||
chat.check('Gated tools/list responds', tools.length, (v) => v >= 1);
|
||||
|
||||
// Trigger gate decision
|
||||
await chat.callTool('begin_session', { description: 'audit smoke test' });
|
||||
|
||||
@@ -167,11 +167,21 @@ describe('Smoke: Backup completeness', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('backup servers have command field for docker-image STDIO servers', () => {
|
||||
it('backup preserves the command of docker-image STDIO servers', async () => {
|
||||
if (!available) return;
|
||||
const dockerStdio = bundle.servers.filter((s) => s.dockerImage && s.transport === 'STDIO');
|
||||
for (const s of dockerStdio) {
|
||||
expect(s.command, `${s.name} (dockerImage STDIO) should have command`).toBeTruthy();
|
||||
// `command` is optional for dockerImage servers (the image entrypoint is
|
||||
// used when it is empty — calcmcp, dav-michal, jmap-michal all run that
|
||||
// way), so requiring it universally asserted a false invariant. What the
|
||||
// backup MUST do is carry the command through for servers that have one.
|
||||
const live = (await mcpdRequest<Server[]>('GET', '/api/v1/servers')).data;
|
||||
const withCommand = live.filter(
|
||||
(s) => s.dockerImage && s.transport === 'STDIO' && Array.isArray(s.command) && s.command.length > 0,
|
||||
);
|
||||
expect(withCommand.length, 'no live dockerImage STDIO server has a command — fidelity unverifiable').toBeGreaterThan(0);
|
||||
for (const s of withCommand) {
|
||||
const backed = bundle.servers.find((b) => b.name === s.name);
|
||||
expect(backed, `${s.name} missing from backup`).toBeTruthy();
|
||||
expect(backed!.command, `${s.name}: backup dropped its command`).toEqual(s.command);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -108,23 +108,38 @@ describe('Smoke: ProxyModel pipeline', () => {
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
|
||||
it('gated session: tools/list returns only begin_session', async () => {
|
||||
it('gated session: tools/list returns begin_session plus only gate-exempt tools', async () => {
|
||||
if (!serverResponding) return;
|
||||
|
||||
// Since the ungated-global system project (PR #137), servers attached to
|
||||
// it ride every project PRE-gate: a fresh gated session sees their tools
|
||||
// alongside begin_session. Anything else leaking through is a gate bug.
|
||||
let exemptServers: string[] = [];
|
||||
try {
|
||||
const raw = await mcpctl('get project ungated-global -o json');
|
||||
const parsed = JSON.parse(raw) as Array<{ servers?: string[] }>;
|
||||
exemptServers = parsed[0]?.servers ?? [];
|
||||
} catch {
|
||||
// Project deleted by an admin (it is deletable) — nothing is exempt.
|
||||
}
|
||||
|
||||
const session = new SmokeMcpSession(PROJECT_NAME);
|
||||
const chat = new ChatReporter(session);
|
||||
chat.section('Gating: fresh session sees only begin_session');
|
||||
chat.section('Gating: fresh session sees begin_session + gate-exempt tools only');
|
||||
try {
|
||||
await chat.initialize();
|
||||
await chat.sendNotification('notifications/initialized');
|
||||
const tools = await chat.listTools();
|
||||
const names = tools.map((t) => t.name);
|
||||
const isExempt = (n: string): boolean => exemptServers.some((s) => n.startsWith(`${s}_`));
|
||||
const leaked = names.filter((n) => n !== 'begin_session' && !isExempt(n));
|
||||
|
||||
chat.check('Exactly 1 tool', tools.length, (v) => v === 1);
|
||||
chat.check('Tool is begin_session', tools[0]?.name ?? '', (v) => v === 'begin_session');
|
||||
chat.check('Has inputSchema', !!tools[0]?.inputSchema, (v) => v === true);
|
||||
chat.check('begin_session present', names.includes('begin_session'), (v) => v === true);
|
||||
chat.check('No gated tools leaked', leaked.length, (v) => v === 0);
|
||||
chat.check('Has inputSchema', !!tools.find((t) => t.name === 'begin_session')?.inputSchema, (v) => v === true);
|
||||
|
||||
expect(tools).toHaveLength(1);
|
||||
expect(tools[0]!.name).toBe('begin_session');
|
||||
expect(names).toContain('begin_session');
|
||||
expect(leaked, 'tools visible before begin_session that are not gate-exempt').toEqual([]);
|
||||
} finally {
|
||||
await chat.close();
|
||||
}
|
||||
|
||||
@@ -30,7 +30,10 @@ interface Tool {
|
||||
|
||||
|
||||
/** Tools served by the gate plugin — intercepted before the pipeline, so exempt. */
|
||||
const GATE_TOOLS = new Set(['begin_session', 'read_prompts', 'propose_prompt', 'propose_skill']);
|
||||
// mcplocal-virtual tools: dispatched by the router before the pipeline, so
|
||||
// they never produce paginatable results and rightly skip the drill-down
|
||||
// contract. get_current_time is the ambient-context clock.
|
||||
const GATE_TOOLS = new Set(['begin_session', 'read_prompts', 'propose_prompt', 'propose_skill', 'get_current_time']);
|
||||
|
||||
describe('Smoke: tool drill-down contract', () => {
|
||||
let available = false;
|
||||
|
||||
Reference in New Issue
Block a user