Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 25 additions & 8 deletions packages/cli/src/ui/commands/goalCommand.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type { Config } from '@qwen-code/qwen-code-core';
import {
__resetActiveGoalStoreForTests,
clearActiveGoal,
getActiveGoal,
notifyGoalTerminal,
} from '@qwen-code/qwen-code-core';

Expand Down Expand Up @@ -86,14 +87,30 @@ describe('goalCommand', () => {
expect((result as { content: string }).content).toMatch(/disabled/i);
});

it('rejects oversized conditions', async () => {
const ctx = createMockCommandContext({
services: { config: makeConfig() as unknown as Config },
});
const result = await goalCommand.action!(ctx, 'x'.repeat(4001));
expect(result).toMatchObject({ type: 'message', messageType: 'error' });
expect((result as { content: string }).content).toMatch(/limited/i);
});
it.each(['interactive', 'non_interactive', 'acp'] as const)(
'accepts conditions longer than 4,000 characters in %s mode',
async (executionMode) => {
const ctx = createMockCommandContext({
executionMode,
services: { config: makeConfig() as unknown as Config },
});
const condition = `${'x'.repeat(4_001)}-goal-condition-end`;

const result = await goalCommand.action!(ctx, condition);

expect(result).toMatchObject({ type: 'submit_prompt' });
const submit = result as { content: Array<{ text: string }> };
expect(submit.content[0].text).toContain(condition);
expect(getActiveGoal('sess-1')?.condition).toBe(condition);
expect(
(ctx.ui.addItem as ReturnType<typeof vi.fn>).mock.calls[0][0],
).toMatchObject({
type: 'goal_status',
kind: 'set',
condition,
});
},
);

it('clears existing goal on clear keyword and emits a cleared card', async () => {
const cfg = makeConfig();
Expand Down
13 changes: 2 additions & 11 deletions packages/cli/src/ui/commands/goalCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,6 @@ const CLEAR_KEYWORDS = new Set([
'cancel',
]);

const MAX_GOAL_LENGTH = 4000;

// Keep the surrounding `"…"` quote structure intact: collapse newlines so the
// condition stays on one line, and downgrade embedded double-quotes to single
// quotes so they don't visually close the wrapping quote.
Expand Down Expand Up @@ -165,14 +163,7 @@ export const goalCommand: SlashCommand = {
return;
}

// ── Branch 3: length cap ─────────────────────────────────────────────
if (q.length > MAX_GOAL_LENGTH) {
return errorMessage(
`Goal condition is limited to ${MAX_GOAL_LENGTH} characters (got ${q.length}).`,
);
}

// ── Branch 4: gates ──────────────────────────────────────────────────
// ── Branch 3: gates ──────────────────────────────────────────────────
if (!config.isTrustedFolder()) {
return errorMessage(
'/goal is only available in trusted workspaces. Trust this folder via `/trust` and try again.',
Expand All @@ -189,7 +180,7 @@ export const goalCommand: SlashCommand = {
);
}

// ── Branch 5: register hook + emit set card + kick off first turn ────
// ── Branch 4: register hook + emit set card + kick off first turn ────
let registered;
try {
registered = registerGoalHook({
Expand Down
17 changes: 17 additions & 0 deletions packages/core/src/goals/goalJudge.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,23 @@ describe('judgeGoal', () => {
expect(text).not.toContain('Condition: done"');
});

it('does not truncate long conditions in the judge prompt', async () => {
const client = makeMockClient({});
const config = makeConfig({ client });
const condition = `${'x'.repeat(4_001)}-goal-condition-end`;

await judgeGoal(config, {
condition,
lastAssistantText: 'not done',
signal: new AbortController().signal,
});

const [contents] = client.generateContent.mock.calls[0];
const wrapped = contents.at(-1) as Content;
const text = (wrapped.parts ?? []).map((p) => p.text ?? '').join('');
expect(text).toContain(JSON.stringify(condition));
});

it('uses a bounded history tail without cloning the full session when available', async () => {
const tail: Content[] = [
{ role: 'user', parts: [{ text: 'recent prompt' }] },
Expand Down
Loading