Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
9361943
refactor(composer): make the + menu one entry with one kind of row
Astro-Han Aug 18, 2026
dee42cb
fix(desktop): make the session-mode write independent of the read
Astro-Han Aug 18, 2026
8b693dc
fix(ui): drop the + menu divider when nothing precedes it
Astro-Han Aug 18, 2026
6cb94c6
fix(desktop): send the session mode to the Host as one mutation
Astro-Han Aug 18, 2026
422dd68
docs(ui): say where the composer's styling lives
Astro-Han Aug 18, 2026
580faa0
fix(desktop): refuse an illegal session-mode pair at the IPC boundary
Astro-Han Aug 18, 2026
c759107
test(desktop): make the + menu and session-mode tests fail for their …
Astro-Han Aug 18, 2026
b63ef45
refactor(composer): let Plan and orchestration be two choices again
Astro-Han Aug 19, 2026
9d52187
refactor(composer): drop the neutral mode row nothing needed
Astro-Han Aug 19, 2026
10f75de
feat(goal): let the user arm a Goal from the composer
Astro-Han Aug 18, 2026
ff3dd24
fix(goal): trim the Goal condition where it is persisted
Astro-Han Aug 18, 2026
27049f8
fix(desktop): reuse the arm operation's own shape and bounds
Astro-Han Aug 18, 2026
703b389
fix(desktop): drop the previous Session's Goal on a Session switch
Astro-Han Aug 18, 2026
7475c57
fix(runtime-host): retire epoch 25 for the new Goal arm operation
Astro-Han Aug 19, 2026
3475585
fix(goal): leave a Goal no Turn has carried for the next Turn, not fo…
Astro-Han Aug 19, 2026
7603f13
refactor(goal): drop the Goal token baseline nobody could set
Astro-Han Aug 19, 2026
77a5c1f
fix(desktop): arm the Goal budgets the form is showing
Astro-Han Aug 19, 2026
ff8d2c6
fix(desktop): reconcile goal arm after disconnect
Sun-GLiang Aug 19, 2026
20c1409
Merge upstream/main into fix/3234-control-reconciliation
Sun-GLiang Aug 19, 2026
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
66 changes: 66 additions & 0 deletions apps/desktop/e2e/goal-dialog-budget.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { test, expect, COMPOSER_INPUT } from './fixtures';

/**
* Arming a Goal starts unattended token spending, and the two budgets in this
* dialog are what stops it. A budget the form shows but does not send is
* therefore the one failure this dialog must not have — most sharply when the
* value is dropped rather than altered, because an absent token budget is not
* a smaller ceiling but no ceiling at all.
*
* The assertions read the Goal back from the Host rather than watching the
* bridge call, so they answer what was actually armed.
*/
test('an unsendable budget blocks Start instead of arming a different one', async ({
window: page,
}) => {
// The + menu only offers a Goal for a Session that exists, so seed one and
// let its Turn settle first — a live Turn disables the entry too.
const composer = page.locator(COMPOSER_INPUT);
await composer.fill('seed session');
await composer.press('Enter');
await expect(page.getByRole('log').getByText(/Fake backend received: seed session/)).toBeVisible();
await expect(page.getByRole('button', { name: '重新生成' })).toHaveCount(1, { timeout: 20_000 });

const sessionId = await page.evaluate(async () => (await window.maka.sessions.list())[0]?.id);
expect(sessionId).toBeTruthy();
const armedGoal = () =>
page.evaluate(async (id: string) => await window.maka.goal.get(id), sessionId as string);

await page.getByRole('button', { name: '添加上下文' }).click();
await page.getByRole('menuitem', { name: '设定 Goal…' }).click();

const dialog = page.getByRole('dialog');
await dialog.getByLabel(/达成条件/).fill('所有测试通过');
const start = dialog.getByRole('button', { name: '开始' });
await expect(start).toBeEnabled();

// Below the Host's own minimum. The field this replaced kept such text to
// itself and left the sent budget null, so Start stayed enabled and armed no
// ceiling at all.
await dialog.getByLabel(/Token 预算/).fill('500');
await expect(start).toBeDisabled();
await expect(dialog.getByText(/请填不小于 1000 的整数/)).toBeVisible();

await dialog.getByLabel(/Token 预算/).fill('5000');
await expect(start).toBeEnabled();

// Above the Host's ceiling on turns; the same rule from the other side.
await dialog.getByLabel(/最多轮数/).fill('250');
await expect(start).toBeDisabled();
await expect(await armedGoal()).toBeNull();

await dialog.getByLabel(/最多轮数/).fill('25');
await expect(start).toBeEnabled();
await start.click();

await expect
.poll(async () => {
const goal = await armedGoal();
return goal && {
condition: goal.condition,
maxIterations: goal.maxIterations,
tokenBudget: goal.tokenBudget,
};
})
.toEqual({ condition: '所有测试通过', maxIterations: 25, tokenBudget: 5000 });
});
81 changes: 81 additions & 0 deletions apps/desktop/src/main/__tests__/goal-arm-outcome.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import type { GoalArmOutcome } from '../../shared/goal-arm.js';
import { interpretGoalArmOutcome } from '../../renderer/goal-arm-outcome.js';
import { getShellCopy } from '../../renderer/locales/shell-copy.js';

test('successful Goal arming closes while every reconciliation result locks the form', () => {
const goal = {
id: 'goal-1',
revision: 1,
sessionId: 'session-1',
condition: 'All tests pass',
status: 'active' as const,
setAt: 1,
iterations: 0,
maxIterations: 50,
consecutiveNoProgress: 0,
blockCap: 8,
tokensAtStart: 0,
tokensNow: 0,
tokensBaselinePending: false,
};
const cases: Array<{
outcome: GoalArmOutcome;
expected: ReturnType<typeof interpretGoalArmOutcome>;
}> = [
{
outcome: { kind: 'armed', goal },
expected: { action: 'close' },
},
{
outcome: {
kind: 'reconciled',
currentGoal: goal,
matchesRequestedState: true,
},
expected: { action: 'lock', notice: { kind: 'matching_goal', goal } },
},
{
outcome: {
kind: 'reconciled',
currentGoal: goal,
matchesRequestedState: false,
},
expected: { action: 'lock', notice: { kind: 'different_goal', goal } },
},
{
outcome: {
kind: 'reconciled',
currentGoal: null,
matchesRequestedState: false,
},
expected: { action: 'lock', notice: { kind: 'no_goal' } },
},
{
outcome: { kind: 'reconciliation_unavailable' },
expected: { action: 'lock', notice: { kind: 'unavailable' } },
},
];

for (const { outcome, expected } of cases) {
assert.deepEqual(interpretGoalArmOutcome(outcome), expected);
}
});

test('Goal reconciliation copy explains authoritative state in Chinese and English', () => {
const zh = getShellCopy('zh').goalDialog;
assert.match(
zh.reconciledMatching('所有测试通过', zh.statusLabels.active),
/所有测试通过.*进行中.*无法确认.*提交/,
);
assert.match(zh.reconciledNoGoal, /未读到 Goal/);
assert.match(zh.reconciliationUnavailable, /不会重复提交/);

const en = getShellCopy('en').goalDialog;
assert.match(
en.reconciledDifferent('All tests pass', en.statusLabels.paused),
/All tests pass.*Paused.*differs/,
);
assert.match(en.reconciliationUnavailable, /will not submit twice/);
});
Loading