Merge commit '70396085b141370ce32de1be4e225b4384eaf46d' into HEAD
# Conflicts: # .agents/notes/implemented/feature/2026-07-07-mcp-client-plugin.i18n.yaml # .agents/notes/implemented/feature/2026-07-20-code-mode-typed-tool-returns.i18n.yaml # .agents/notes/implemented/feature/2026-07-22-web-multimodal-image-input-and-durable-attachments.i18n.yaml # .agents/notes/implemented/feature/2026-07-22-web-multimodal-image-input-and-durable-attachments.md # .agents/notes/implemented/feature/2026-07-22-web-multimodal-image-input-and-durable-attachments.zh.md # .agents/notes/implemented/simplification/2026-07-23-acp-automation-only-protocol.i18n.yaml # docs/config-catalog.i18n.yaml # docs/module-graph.i18n.yaml # docs/module-graph.md # docs/module-graph.zh.md # docs/tool-catalog.i18n.yaml # docs/tool-catalog.md # docs/tool-catalog.zh.md # examples/acp-agent/tests/snapshots/advanced-toolchain/tool-schemas.expected.json # examples/acp-agent/tests/snapshots/both-mode-turn/tool-schemas.expected.json # examples/acp-agent/tests/snapshots/code-mode-turn/tool-schemas.expected.json # packages/core/tools/README.i18n.yaml # packages/core/tools/README.zh.md # packages/core/tools/src/code-mode.ts # packages/host/apiproxy/tests/api-proxy-models.spec.ts # packages/host/plugin-inventory/tests/inventory.spec.ts # packages/mcp/mcp-client/tests/mcp-client.e2e.ts # packages/mcp/mcp-client/tests/mcp-client.spec.ts # packages/self-modification/tool-cordis/src/api-catalog.ts # packages/test-support/acp-snapshot/README.i18n.yaml # pnpm-lock.yaml
This commit is contained in:
@@ -34,14 +34,20 @@ describe('CI workflow', () => {
|
||||
|| !isRecord(workflow.jobs['windows-native'])
|
||||
|| !isRecord(workflow.jobs['wine-apt-cache'])
|
||||
|| !isRecord(workflow.jobs['serial-windows'])
|
||||
|| !isRecord(workflow.jobs['node-24'])
|
||||
|| !isRecord(workflow.jobs['node-24-coverage'])
|
||||
|| !isRecord(workflow.jobs['node-24-consumers'])
|
||||
|| !isRecord(workflow.jobs['all-checks-passed'])) {
|
||||
throw new TypeError('CI workflow must define windows, windows-native, wine-apt-cache, serial-windows, and all-checks-passed jobs')
|
||||
throw new TypeError('CI workflow must define windows, windows-native, wine-apt-cache, serial-windows, node-24, node-24-coverage, node-24-consumers, and all-checks-passed jobs')
|
||||
}
|
||||
|
||||
const windows = workflow.jobs.windows
|
||||
const windowsNative = workflow.jobs['windows-native']
|
||||
const wineAptCache = workflow.jobs['wine-apt-cache']
|
||||
const serialWindows = workflow.jobs['serial-windows']
|
||||
const node24 = workflow.jobs['node-24']
|
||||
const node24Coverage = workflow.jobs['node-24-coverage']
|
||||
const node24Consumers = workflow.jobs['node-24-consumers']
|
||||
const aggregate = workflow.jobs['all-checks-passed']
|
||||
if (!Array.isArray(windows.steps) || !Array.isArray(aggregate.needs)) {
|
||||
throw new TypeError('Windows job must define steps and the aggregate must define needs')
|
||||
@@ -57,8 +63,10 @@ describe('CI workflow', () => {
|
||||
expect(commandSteps.some(step => step.run.includes('wine-windows-gates.sh'))).toBe(true)
|
||||
|
||||
// windows-native: non-blocking native job with failover, runs windows-complete.
|
||||
// Its pool is resolved by the Windows-specific switch.
|
||||
expect(typeof windowsNative['runs-on']).toBe('string')
|
||||
expect(windowsNative['runs-on']).toContain('DSH_CI_FAILOVER')
|
||||
expect(windowsNative['runs-on']).toContain('DSH_CI_FAILOVER_WINDOWS')
|
||||
expect(windowsNative['runs-on']).not.toContain('DSH_CI_FAILOVER_LINUX')
|
||||
expect(windowsNative['runs-on']).toContain('self-hosted')
|
||||
expect(windowsNative['runs-on']).toContain('dsh-win-ci')
|
||||
expect(windowsNative['runs-on']).toContain('dsh-windows-2025-16core')
|
||||
@@ -85,14 +93,94 @@ describe('CI workflow', () => {
|
||||
expect(aggregate.needs).toContain('windows')
|
||||
expect(aggregate.needs).not.toContain('windows-native')
|
||||
expect(aggregate.needs).not.toContain('serial-windows')
|
||||
|
||||
// Linux failover is a separate switch: the three required Linux workers
|
||||
// and the verdict job resolve their pool through DSH_CI_FAILOVER_LINUX,
|
||||
// never the Windows switch.
|
||||
for (const [jobName, job] of [['node-24', node24], ['node-24-coverage', node24Coverage], ['node-24-consumers', node24Consumers]] as const) {
|
||||
expect(typeof job['runs-on']).toBe('string')
|
||||
expect(job['runs-on'], `${jobName} runs-on must use the Linux failover switch`).toContain('DSH_CI_FAILOVER_LINUX')
|
||||
expect(job['runs-on'], `${jobName} runs-on must not use the Windows failover switch`).not.toContain('DSH_CI_FAILOVER_WINDOWS')
|
||||
expect(job['runs-on']).toContain('vm-backup')
|
||||
}
|
||||
expect(aggregate['runs-on']).toContain('DSH_CI_FAILOVER_LINUX')
|
||||
expect(aggregate['runs-on']).not.toContain('DSH_CI_FAILOVER_WINDOWS')
|
||||
expect(aggregate['runs-on']).toContain('vm-backup')
|
||||
})
|
||||
|
||||
it('exempts push from cancellation, so one master merge does not cancel the running drill', () => {
|
||||
const workflow = loadWorkflow('.github/workflows/ci.yml')
|
||||
if (!isRecord(workflow.jobs) || !isRecord(workflow.concurrency)) {
|
||||
throw new TypeError('CI workflow must define jobs and a workflow-level concurrency block')
|
||||
}
|
||||
|
||||
// Cancellation applies to the whole superseded RUN, so this has to be
|
||||
// decided at workflow level and gated on the event: a job-level group
|
||||
// cannot exempt its job from its run being cancelled. Only push is exempt —
|
||||
// a drill takes longer than the interval between master merges. The negated
|
||||
// form is load-bearing: `== 'pull_request'` would also stop cancelling
|
||||
// workflow_dispatch, and a re-dispatched runner benchmark holds up to 12
|
||||
// larger runners for 15 minutes in this same group on master. The
|
||||
// expression is evaluated against the NEWLY TRIGGERED run, so a dispatch on
|
||||
// master still cancels a mid-flight drill; the runbook records that bound.
|
||||
expect(workflow.concurrency['cancel-in-progress']).toBe("${{ github.event_name != 'push' }}")
|
||||
|
||||
// Neither drill may carry a job-level group: it would not exempt the job
|
||||
// from run-scoped cancellation.
|
||||
for (const name of ['serial-linux-selfhosted', 'serial-windows']) {
|
||||
const job = workflow.jobs[name]
|
||||
if (!isRecord(job)) throw new TypeError(`${name} must be defined`)
|
||||
expect(job.concurrency).toBeUndefined()
|
||||
// Both stay master-push-only; that is what makes the push carve-out safe.
|
||||
expect(job.if).toBe("github.event_name == 'push' && github.ref == 'refs/heads/master'")
|
||||
}
|
||||
|
||||
// What bounds the cost of exempting push: a master push may only carry the
|
||||
// cache seeder and the two drills. Any job reachable on push would start
|
||||
// accumulating uncancelled runs, so the set is pinned here.
|
||||
//
|
||||
// Classification is an exact allowlist of the conditions in use, not a
|
||||
// substring match: `github.event_name != 'pull_request'` mentions
|
||||
// `pull_request` yet IS push-reachable, so matching on the event name alone
|
||||
// would silently misclassify it as gated.
|
||||
const NOT_PUSH_REACHABLE = new Set([
|
||||
"github.event_name == 'pull_request'",
|
||||
"always() && github.event_name == 'pull_request'",
|
||||
"github.event_name == 'workflow_dispatch' && inputs.suite == 'larger-runner-benchmark'",
|
||||
"github.event_name == 'workflow_dispatch' && inputs.suite == 'consolidated-runner-benchmark'",
|
||||
])
|
||||
const pushReachable = Object.entries(workflow.jobs)
|
||||
.filter(([, job]) => {
|
||||
if (!isRecord(job)) return false
|
||||
if (job.if === undefined) return true // unconditional: runs on every event
|
||||
if (job.if === false) return false // `if: false` parses as a boolean
|
||||
if (typeof job.if !== 'string') return true // unrecognized shape: surface it
|
||||
return !NOT_PUSH_REACHABLE.has(job.if.trim())
|
||||
})
|
||||
.map(([name]) => name)
|
||||
.sort()
|
||||
expect(pushReachable).toEqual(['serial-linux-selfhosted', 'serial-windows', 'wine-apt-cache'])
|
||||
|
||||
// Why workflow_dispatch must keep cancelling: each benchmark fans out to a
|
||||
// dozen larger runners at once, in this same group on master. If it stopped
|
||||
// cancelling, a re-dispatch would queue ahead of a drill instead of
|
||||
// replacing the stale measurement.
|
||||
for (const name of ['larger-runner-benchmark', 'consolidated-runner-benchmark']) {
|
||||
const job = workflow.jobs[name]
|
||||
if (!isRecord(job) || !isRecord(job.strategy)) {
|
||||
throw new TypeError(`${name} must define a matrix strategy`)
|
||||
}
|
||||
expect(job.strategy['max-parallel']).toBe(12)
|
||||
expect(job['timeout-minutes']).toBe(15)
|
||||
}
|
||||
})
|
||||
|
||||
it('keeps supported LSP source under native Windows coverage', () => {
|
||||
const config = readFileSync(resolve(root, 'vitest.config.ts'), 'utf8')
|
||||
|
||||
expect(config).not.toContain('packages/lsp/lsp-local/src/connection.ts')
|
||||
expect(config).not.toContain('packages/lsp/lsp-local/src/index.ts')
|
||||
expect(config).not.toContain('packages/lsp/lsp-local/src/instance.ts')
|
||||
expect(config).not.toContain('packages/lsp/lsp-stdio/src/connection.ts')
|
||||
expect(config).not.toContain('packages/lsp/lsp-stdio/src/index.ts')
|
||||
expect(config).not.toContain('packages/lsp/lsp-stdio/src/instance.ts')
|
||||
})
|
||||
|
||||
it('requires one release-shaped Python runtime target on every pull request', () => {
|
||||
|
||||
Reference in New Issue
Block a user