Files
orca/config/scripts/pr-unit-support-routing.test.mjs
T
Brennan Benson f06f737802 Unify Source Control action permissions and card layout (#30449)
* Source control actions start chats and terminals on their saved model

A "start an agent" action's model lived only in free-text CLI arguments, which a native chat
never reads, so AI Fix opened a chat on the default model. Each action now saves a structured
model and effort that both routes apply: chat create options, or the agent's own flags in a
terminal (winning over the same flag in CLI arguments). Desktop, dialog, fix-checks and mobile
launches carry it; Settings rows get Model/Effort selects and the CLI arguments field says it is
terminal-only.

Tracked in STA-11102.

* Name the model in the repository override note

* Type the dialog's model hand-off; keep Agent in English in Japanese copy

* fix: preserve action chat model choices through launch

* Preserve action selection ownership across chat launches

* Keep terminal host contract fixtures aligned with preflight

* fix: wait for action Settings agent inventory

* fix: consolidate direct agent routing imports

* fix: use standard action option labels

* fix: align action launch contracts with shared types

* fix: sync action option localization metadata

* test: provide action Settings inventory in mounted fixtures

* Keep action options row height stable while loading

* Let each Source Control action choose its chat permission mode

An action's saved chat choice may now carry a permission mode, with or without a model;
"Same as new chats" saves nothing. A model is replaced only when the action names one,
so a permission-only choice keeps the chat's remembered model and options.

* fix(native-chat): leave older hosts their own permission mode and keep action options readable

- An action's saved permission mode is sent only to hosts that advertise
  agentSession.create.options-mode.v1. Older hosts get no mode, in create
  options or a later setOption, so the chat starts on that host's new-chat
  default, as a phone launch to such a host already does. Hosts from
  1.4.225 to 1.4.227 refused a mode their tables lacked (OpenCode, Grok,
  OMP), leaving the action unable to start.
- Settings action rows give Model and Permissions full rows in the options
  column so saved values are readable; the loading placeholder reserves the
  same rows.
- Remove two unused locale keys; fix two stale comments.
- Tests: the older-host create carries no permission mode; a permission
  mode picked alone keeps the remembered model on an acquired create.

* fix(settings): reserve one options row while loading an action with nothing saved

Terminal and permissions-only rows no longer shrink when loading ends; a chat
agent with a model list may grow by its Model row once known.

* fix(native-chat): keep the create options mode a checked literal type

* fix(settings): share verified action option fields

* fix(settings): keep action selectors on standard typography

* Keep the launch files under their length limits after the main merge

The structured route imports its result types where they are defined, and the two older-host
action-launch tests share one setup helper.

* One action settings section for chat and terminal launches

A Source Control action's Model, model options and Permissions now apply whether it opens a
chat or a terminal; terminals receive them as the agent's own flags, and a repository's own
CLI arguments keep the flags it sets. Extra CLI arguments stay terminal-only.

* fix(source-control-ai): one terminal projection for saved action choices

Ask in a terminal now states its permission instead of leaving it to the
user's own config: Claude and Grok pass --permission-mode default, Codex
passes -a on-request -s read-only -c approvals_reviewer="user" (the
interactive CLI has no untrusted policy, so a read-only sandbox is what
asks before edits). Agents whose CLI has no flag for a mode show the
existing "Applies to chats only" hint.

The strict launch-preferences projection now requires the launch agent at
every caller, composes model ids through the resolver's own step, carries
a mode only where the agent's terminal has a flag for it, and the local
string wire keeps booleans as 'true'/'false', so Cursor Thinking/Fast reach
the terminal and an OpenCode permission-only choice is not refused.

The argument parser knows flag arity, so removing a boolean permission
flag keeps other options' values, following positionals and '--'.
Pre-admission and executor share one terminal admission check on the
options the terminal will receive. Customize with Save launches the recipe
as it will resolve once saved; legacy partial repository overrides are
edited as the complete recipe they resolve to. Empty Extra CLI arguments
start collapsed.

* fix(source-control-ai): an edited repository action saves complete

Every Settings edit of a repository action (agent, model, permission,
prompt or arguments) now starts from the complete recipe the action
resolves to, whether its stored override is owned or a legacy partial.
The resolved agent selection is kept (never an agent only detected to run
a default), an explicit null args clear stays null, and empty args stay
empty instead of being dropped. Clearing the last preference therefore
means the action agent's own defaults rather than re-inheriting Global.

The action text draft is lossless: unset or cleared prompt and args stay
so until the user types, and the inputs show them as empty, so saving a
prompt no longer writes empty args that suppress the agent's default
flags. Untouched sibling actions are not rewritten by the edit.

* fix(settings): style the Extra CLI arguments toggle through Button's own variant and size

CI's changed-code quality check refuses restyle classes on Button and
CollapsibleContent. Use the ghost variant at size xs with a margin, and
put the content spacing on a plain inner wrapper.

* fix: keep saved action and restart launch recipes consistent

* fix: describe inherited arguments and chat-only permissions accurately

* test: check repository action settings fixture type

* test: map saved action options into terminal compiler inputs

* fix: retain saved action controls during agent detection

* fix(i18n): retire obsolete Portuguese action labels

* test: align folder launch fallback mock with outcome export

* fix: carry resolved connector launch intent and mode together

* test: pin shared action launch branches to recorded agent output

* test: preserve saved source control choices through detection and migration

* test: align launch fixtures with integrated result contract

* test: retain recipe and restart contracts after main integration

* Unify Source Control action permission settings

* Keep native Ask copy with its owning change

* Preserve action drafts and old-host permission intent

* Consume rejected Source Control action saves

* Follow the moved Settings persistence fixture

* Keep permission integration contracts in the cross-version test project

* Include the action permission inheritance label in English catalogs

* Align launch contracts with shared permission recipes

* Keep Source Control action controls within their Settings columns

* Use primitive variants for compact action settings controls

* Show action arguments only for terminal defaults and widen recipe rows

* Apply terminal-only action arguments visibility to repository overrides

* Align action permission control with model controls

* Use shared settings label for action permissions

* Hide action model controls until argument migration ships
2026-10-26 16:01:56 -07:00

160 lines
6.3 KiB
JavaScript

import { beforeAll, describe, expect, it } from 'vitest'
import { classifyPrJobs } from './pr-code-change-scope.mjs'
import { selectPrE2eSpecs } from './pr-e2e-source-routing.mjs'
import {
buildUnitDependencyGraph,
collectUnitDependencyGraph,
unitConsumers
} from './ci-unit-dependency-graph.mjs'
import { CROSS_VERSION_WIRE_DIR, discoverUnitFiles } from './ci-unit-files.mjs'
import { planUnitSelection } from './ci-unit-selection.mjs'
const harness = 'src/renderer/src/runtime/web-session-tabs-sync-test-harness.ts'
const structuredSessionHarness =
'src/renderer/src/components/native-chat/NativeChatStructuredSession.test-harness.tsx'
const unitHarnesses = [
harness,
structuredSessionHarness,
'src/renderer/src/store/slices/editor-slice-test-harness.ts',
'src/renderer/src/store/slices/browser-slice-test-harness.ts',
'src/renderer/src/runtime/web-runtime-session-test-harness.ts',
'src/renderer/src/runtime/sync-runtime-graph-test-harness.ts'
]
const supportBridges = new Set([
'src/renderer/src/runtime/web-runtime-browser-creation-placement-test-rig.ts',
'src/renderer/src/components/editor/editor-model-lifetime-fixture.ts',
// Autosave suites' store rig; it takes the editor harness's tab-layout maps.
'src/renderer/src/components/editor/editor-autosave-controller-test-fixture.ts',
// Closed-saving-view save suites' rig; it builds on the autosave rig above.
'src/renderer/src/components/editor/editor-save-closed-view.test-fixture.ts',
// Browser suites' store with main drawing their tabs; it takes the browser harness's store.
'src/renderer/src/store/slices/browser-slice-drawn-from-main.test-fixture.ts'
])
function productConsumers(file, graph, unitFiles) {
return [...unitConsumers([file], graph.reverse)].filter(
(consumer) =>
consumer !== file &&
!unitFiles.has(consumer) &&
!(consumer.startsWith(CROSS_VERSION_WIRE_DIR) && consumer.endsWith('.unit.test.ts')) &&
!supportBridges.has(consumer)
)
}
describe('unit support routing', () => {
it.each(unitHarnesses)(
'keeps unit checks and lint while excluding packaging for %s',
(unitHarness) => {
const result = classifyPrJobs([unitHarness])
expect(result.should_run).toBe(true)
expect(result.static_analysis).toBe(true)
expect(result.typecheck).toBe(true)
expect(result.test).toBe(true)
expect(result.package).toBe(false)
expect(result.package_windows).toBe(false)
}
)
it.each(unitHarnesses)(
'retains packaging for unknown helpers and mixed product changes with %s',
(unitHarness) => {
for (const file of [
unitHarness.replace(/\.(tsx?)$/, '-next.$1'),
'src/renderer/src/runtime/web-session-tabs-sync-next-test-harness.ts',
'src/renderer/src/runtime/web-session-tabs-sync/snapshot-api.ts',
'src/renderer/src/components/native-chat/NativeChatStructuredSession.next-test-harness.tsx',
'src/renderer/src/components/native-chat/NativeChatStructuredSession.tsx',
'src/main/providers/windows-conpty-wide-char-duplication.node-pty.test.ts',
'src/main/browser/browser-route-webrtc-egress.electron.test.ts'
]) {
const result = classifyPrJobs([unitHarness, file])
expect(result.should_run, file).toBe(true)
expect(result.test, file).toBe(true)
expect(result.package, file).toBe(
file !== 'src/main/providers/windows-conpty-wide-char-duplication.node-pty.test.ts'
)
expect(result.package_windows, file).toBe(true)
}
}
)
it.each(unitHarnesses)('does not select product E2E for %s', (unitHarness) => {
expect(selectPrE2eSpecs([unitHarness])).toEqual([])
})
it('retains E2E selection for unknown helpers, product changes and selected specs', () => {
const spec = 'tests/e2e/paired-client-hosted-browser-restart-survival.spec.ts'
for (const file of [
'src/renderer/src/runtime/web-session-tabs-sync-next-test-harness.ts',
'src/renderer/src/runtime/web-session-tabs-sync/snapshot-api.ts',
spec
]) {
expect(selectPrE2eSpecs([harness, file]), file).toContain(spec)
}
})
})
describe('unit support import ownership', () => {
let graph
let unitFiles
beforeAll(() => {
graph = collectUnitDependencyGraph()
unitFiles = new Set(discoverUnitFiles())
}, 60_000)
it.each(unitHarnesses)('rejects any product consumer of %s', (unitHarness) => {
expect(graph.files.has(unitHarness)).toBe(true)
expect(
[...unitConsumers([unitHarness], graph.reverse)].some((file) => unitFiles.has(file))
).toBe(true)
expect(productConsumers(unitHarness, graph, unitFiles)).toEqual([])
})
it.each(unitHarnesses)('keeps the full unit inventory for a normal PR editing %s', (file) => {
const files = [...unitFiles]
const plan = planUnitSelection({
files,
changed: [file],
graph,
timings: {},
event: { pull_request: { draft: false } },
mode: 'selected'
})
expect(plan.mode).toBe('shadow')
expect(plan.executionFiles).toEqual(files)
expect(plan.shards).toHaveLength(5)
})
it.each([
"import '@/runtime/web-session-tabs-sync-test-harness'",
"export * from '@/runtime/web-session-tabs-sync-test-harness'",
"import('@/runtime/web-session-tabs-sync-test-harness')",
"import type { Fixture } from '@/runtime/web-session-tabs-sync-test-harness'"
])('detects a product importer using %s', (source) => {
const importer = 'src/renderer/src/runtime/product.ts'
const test = 'src/renderer/src/runtime/ownership.test.ts'
const synthetic = buildUnitDependencyGraph(
new Map([
[harness, 'export const value = 1'],
[importer, source],
[test, "import './web-session-tabs-sync-test-harness'"]
])
)
expect(productConsumers(harness, synthetic, new Set([test]))).toEqual([importer])
})
it('rejects product consumers reached through an audited support bridge', () => {
const bridge = 'src/renderer/src/components/editor/editor-model-lifetime-fixture.ts'
const importer = 'src/renderer/src/components/editor/product.ts'
const synthetic = buildUnitDependencyGraph(
new Map([
[harness, 'export const value = 1'],
[bridge, "import '@/runtime/web-session-tabs-sync-test-harness'"],
[importer, "import './editor-model-lifetime-fixture'"]
])
)
expect(productConsumers(harness, synthetic, new Set())).toEqual([importer])
})
})