Compare commits

...

3 Commits

Author SHA1 Message Date
ecd86fa357 Merge pull request 'smoke: reconcile assertions with ungated-global + ambient clock' (#140) from fix/smoke-ungated-global-reconcile into main
Some checks failed
CI/CD / lint (push) Successful in 1m16s
CI/CD / test (push) Successful in 1m27s
CI/CD / typecheck (push) Successful in 3m11s
CI/CD / smoke (push) Failing after 1m59s
CI/CD / build (push) Successful in 2m25s
CI/CD / publish (push) Has been skipped
2026-10-09 15:06:39 +00:00
Michal
ed54aa1464 fix(smoke): reconcile assertions with ungated-global + ambient clock
Some checks failed
CI/CD / lint (pull_request) Successful in 1m19s
CI/CD / test (pull_request) Successful in 1m27s
CI/CD / typecheck (pull_request) Successful in 3m12s
CI/CD / smoke (pull_request) Failing after 2m0s
CI/CD / build (pull_request) Successful in 4m51s
CI/CD / publish (pull_request) Has been skipped
Three smoke tests predated PR #137 (ungated-global pre-gate servers) and
the ambient-context clock, and were never exercised against a prod that
had calcmcp seeded — the 5415060 fix was cut over via kubectl, bypassing
the suite. They failed on today's a783869 deploy:

- proxy-pipeline: a fresh gated session legitimately sees begin_session
  plus the ungated-global project's tools. Now discovers the gate-exempt
  server set and asserts nothing ELSE leaks pre-gate.
- tool-drilldown: get_current_time is a router-dispatched virtual tool
  (ambient clock) that never paginates; added to the exempt set.
- backup: command is optional for dockerImage STDIO servers (entrypoint
  images — calcmcp, dav-michal, jmap-michal run with none). The real
  contract is fidelity: every live server WITH a command must keep it
  through backup. Asserted that instead.
- audit: 'Gated (only begin_session)' label said something the >=1 check
  never verified; renamed.

Full suite against live a783869: 32 files, 181/181 green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-10-09 16:06:24 +01:00
a783869e7c Merge pull request 'agentic-teams: epics, tasks, git forges, agent authority' (#139) from feat/agentic-teams into main
Some checks failed
CI/CD / lint (push) Successful in 1m16s
CI/CD / test (push) Successful in 1m30s
CI/CD / typecheck (push) Successful in 3m15s
CI/CD / smoke (push) Failing after 2m1s
CI/CD / build (push) Successful in 2m16s
CI/CD / publish (push) Has been skipped
2026-10-09 14:47:33 +00:00
4 changed files with 41 additions and 13 deletions

View File

@@ -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' });

View File

@@ -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);
}
});

View File

@@ -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();
}

View File

@@ -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;