This commit is contained in:
@@ -574,7 +574,7 @@ describe('executeMovement parallel tool execution', () => {
|
||||
expect(markerMessages.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
it('force-transitions to defaultNext when initial prompt is oversized and defaultNext is set', async () => {
|
||||
it('aborts when initial prompt is oversized and defaultNext is terminal', async () => {
|
||||
const { ContextManager } = await import('./context-manager.js');
|
||||
const cm = new ContextManager({ limitTokens: 1_000 });
|
||||
|
||||
@@ -594,10 +594,10 @@ describe('executeMovement parallel tool execution', () => {
|
||||
{ contextManager: cm },
|
||||
);
|
||||
|
||||
// makeMovement defaultNext is 'COMPLETE' so we force-transition there
|
||||
expect(result.next).toBe('COMPLETE');
|
||||
expect(result.output).toContain('Context overflow');
|
||||
expect(result.lessons).toContain('Context overflow');
|
||||
// A terminal fallback would report a false completion, so overflow aborts.
|
||||
expect(result.next).toBe('ABORT');
|
||||
expect(result.abortCode).toBe('context_overflow');
|
||||
expect(result.output).toContain('LLM request blocked before send');
|
||||
// Only the isolated summary call should have happened — no main LLM call
|
||||
expect(executeToolMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { execFileSync } from 'child_process';
|
||||
import { tmpdir } from 'os';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'fs';
|
||||
import { existsSync as existsSyncEvents, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import type { MovementResult } from './agent-loop.js';
|
||||
import type { PieceDef } from './piece-runner.js';
|
||||
@@ -521,15 +521,9 @@ describe('buildFollowupNotice (option C)', () => {
|
||||
// Traceability T-2 — handoff / delta / followup / context_action
|
||||
// ============================================================
|
||||
|
||||
import { runPiece } from './piece-runner.js';
|
||||
import { readFileSync } from 'fs';
|
||||
import { createFileEventLogger, parseEventLine, type EventBase } from '../progress/event-log.js';
|
||||
import type { OpenAICompatClient, LLMEvent } from '../llm/openai-compat.js';
|
||||
|
||||
vi.mock('./agent-loop.js', () => ({
|
||||
executeMovement: vi.fn(),
|
||||
}));
|
||||
|
||||
function readAllEvents(workspacePath: string): EventBase[] {
|
||||
const path = join(workspacePath, 'logs', 'events.jsonl');
|
||||
if (!existsSyncEvents(path)) return [];
|
||||
@@ -541,7 +535,6 @@ function readAllEvents(workspacePath: string): EventBase[] {
|
||||
});
|
||||
}
|
||||
|
||||
import { existsSync as existsSyncEvents } from 'fs';
|
||||
|
||||
describe('Traceability T-2: piece-runner emission for subtask boundary + followup', () => {
|
||||
let workspace: string;
|
||||
|
||||
@@ -5,6 +5,14 @@ import { mkdtempSync, rmSync, mkdirSync, writeFileSync, readFileSync, existsSync
|
||||
import { tmpdir } from 'os';
|
||||
import { join } from 'path';
|
||||
import matter from 'gray-matter';
|
||||
// This mock is intentionally file-wide. vi.mock is hoisted, so keeping it at
|
||||
// top level makes the actual test behavior explicit and Vitest 4 compatible.
|
||||
vi.mock('child_process', () => ({
|
||||
execSync: () => {
|
||||
throw new Error('git: command not found');
|
||||
},
|
||||
}));
|
||||
|
||||
|
||||
// ---- helpers ----------------------------------------------------------------
|
||||
|
||||
@@ -134,21 +142,8 @@ describe('silentFork', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ---- git-unavailable suite (isolated via vi.mock) ---------------------------
|
||||
//
|
||||
// vi.mock hoisting means this mock is set up before any import of the module
|
||||
// under test, making execSync throw unconditionally for this describe block.
|
||||
|
||||
// ---- git-unavailable suite --------------------------------------------------
|
||||
describe('silentFork — git unavailable', () => {
|
||||
// Hoist the mock so it applies before the module is evaluated.
|
||||
vi.mock('child_process', () => {
|
||||
return {
|
||||
execSync: () => {
|
||||
throw new Error('git: command not found');
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
let dir: string;
|
||||
let builtinDir: string;
|
||||
let dataDir: string;
|
||||
|
||||
@@ -143,6 +143,13 @@ function isBlockedDocsSubpath(relFromDocs: string): boolean {
|
||||
});
|
||||
}
|
||||
|
||||
function sanitizeListedDescription(description: string): string {
|
||||
const referencesInternalDoc = BLOCKED_DOCS_SUBPATHS.some((blocked) =>
|
||||
description.includes(blocked.replace(/\/$/, '')),
|
||||
);
|
||||
return referencesInternalDoc ? '(description omitted: internal reference)' : description;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map a symbolic doc name to a concrete file path under an allow-listed root.
|
||||
* Returns null on invalid names, attempted traversal, or blocked internal docs.
|
||||
@@ -306,7 +313,7 @@ function extractMarkdownDescription(filePath: string): string {
|
||||
}
|
||||
if (line.startsWith('#')) continue;
|
||||
// Use this line as description
|
||||
return line.slice(0, 140);
|
||||
return sanitizeListedDescription(line.slice(0, 140));
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
@@ -326,7 +333,7 @@ function extractPieceDescription(filePath: string): string {
|
||||
.split('\n')
|
||||
.map((s) => s.trim())
|
||||
.find((s) => s.length > 0);
|
||||
return (first ?? '(no description)').slice(0, 140);
|
||||
return sanitizeListedDescription((first ?? '(no description)').slice(0, 140));
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as dns from 'dns';
|
||||
import { isIP } from 'node:net';
|
||||
import { isPrivateOrForbidden } from '../../../net/ssrf-strict.js';
|
||||
|
||||
// These delegate to the hardened range check in src/net/ssrf-strict.ts so that
|
||||
@@ -6,11 +7,13 @@ import { isPrivateOrForbidden } from '../../../net/ssrf-strict.js';
|
||||
// loopback, RFC1918, link-local + cloud metadata (169.254/16, fd00:ec2::),
|
||||
// CGNAT (100.64/10), 0.0.0.0/8, IPv4-mapped IPv6, NAT64, multicast, reserved.
|
||||
export function isPrivateIPv4(ip: string): boolean {
|
||||
if (isIP(ip) !== 4) return false;
|
||||
return isPrivateOrForbidden(ip, 4);
|
||||
}
|
||||
|
||||
export function isPrivateIPv6(ip: string): boolean {
|
||||
const normalized = ip.toLowerCase().replace(/^\[|\]$/g, '');
|
||||
if (isIP(normalized) !== 6) return false;
|
||||
return isPrivateOrForbidden(normalized, 6);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user