ci: gate merges on deterministic no-AK E2E (#8313)

* test(sdk-typescript): stabilize permission mode round trips

* test(sdk-typescript): make permission control deterministic

* test(sdk-typescript): make orchestration E2E deterministic

* test(sdk-typescript): make orchestration E2E deterministic

* ci: gate merges on deterministic no-AK E2E

* ci: gate merges on deterministic no-AK E2E

* Merge PR #8302 review follow-up

* test(sdk-typescript): distinguish plan-mode enforcement

* test(sdk-typescript): distinguish plan-mode enforcement

* ci: validate stacked gate against main

* ci: validate stacked gate against main

* test(sdk-typescript): reuse fake tool server

* test(sdk-typescript): reuse fake tool server

* test(sdk-typescript): stabilize model switch timing

* test(sdk-typescript): ignore E2E side queries

* fix(ci): enforce no-AK gate invariants

* test(ci): cover no-AK gate event wiring

* fix(ci): isolate no-AK gate environment

* test(ci): anchor credential isolation checks

* fix(ci): complete no-AK gate credential clearing and restore closed-query tests (#8313)

* fix(ci): harden no-AK gate per review feedback (#8313)

* test(ci): assert timeout-minutes on no-AK gate step (#8313)

---------

Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
This commit is contained in:
易良 2026-08-02 10:21:14 +08:00 • committed by GitHub
parent 8d6d2ab56a
commit 7100ffb816
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 349 additions and 458 deletions

View file

@ -408,9 +408,41 @@ jobs:
node -e "const fs = require('node:fs'); for (const key of ['HOME', 'USERPROFILE']) { const dir = process.env[key]; if (dir) fs.mkdirSync(dir, { recursive: true }); }"
npm run test:ci
- name: 'Run no-AK integration smoke tests'
if: "${{ needs.classify_pr.outputs.skip_ci != 'true' && steps.ci_profile.outputs.ci_profile == 'full' && github.event_name == 'pull_request' }}"
run: 'npm run test:integration:no-ak:sandbox:none'
- name: 'Run required no-AK integration gate'
if: "${{ needs.classify_pr.outputs.skip_ci != 'true' && steps.ci_profile.outputs.ci_profile == 'full' && (github.event_name == 'pull_request' || github.event_name == 'merge_group') }}"
timeout-minutes: 20
env:
HOME: '${{ runner.temp }}/qwen-no-ak-home'
USERPROFILE: '${{ runner.temp }}/qwen-no-ak-home'
QWEN_HOME: '${{ runner.temp }}/qwen-no-ak-home/.qwen'
API_KEY: ''
ANTHROPIC_API_KEY: ''
ANTHROPIC_BASE_URL: ''
ANTHROPIC_MODEL: ''
BAILIAN_CODING_PLAN_API_KEY: ''
BAILIAN_TOKEN_PLAN_API_KEY: ''
DEEPSEEK_API_KEY: ''
OPENAI_API_KEY: ''
OPENAI_BASE_URL: ''
OPENAI_MODEL: ''
DASHSCOPE_API_KEY: ''
GOOGLE_API_KEY: ''
GOOGLE_MODEL: ''
QWEN_API_KEY: ''
QWEN_MODEL: ''
GEMINI_API_KEY: ''
GEMINI_MODEL: ''
IDEALAB_API_KEY: ''
MINIMAX_API_KEY: ''
MODELSCOPE_API_KEY: ''
OPENROUTER_API_KEY: ''
REQUESTY_API_KEY: ''
XAI_API_KEY: ''
ZAI_API_KEY: ''
QWEN_DEFAULT_AUTH_TYPE: ''
run: |-
mkdir -p "${HOME}" "${QWEN_HOME}"
npm run test:integration:no-ak:sandbox:none
- name: 'Publish Test Report (for non-forks)'
if: |-

File diff suppressed because it is too large Load diff

View file

@ -203,6 +203,31 @@ describe('System Control (E2E)', () => {
await fakeServer.close();
}
});
it('should throw error when setModel is called on closed query', async () => {
const fakeServer = await startFakeOpenAIServer(
() => ({ content: 'Done.' }),
FAKE_SERVER_OPTIONS,
);
try {
const q = query({
prompt: 'Hello',
options: {
...SHARED_TEST_OPTIONS,
...fakeModelOptions(fakeServer.baseUrl),
cwd: testDir,
},
});
await q.close();
await expect(q.setModel('fake-model-2')).rejects.toThrow(
'Query is closed',
);
} finally {
await fakeServer.close();
}
});
});
describe('supportedCommands API', () => {
@ -284,5 +309,28 @@ describe('System Control (E2E)', () => {
await fakeServer.close();
}
});
it('should throw error when supportedCommands is called on closed query', async () => {
const fakeServer = await startFakeOpenAIServer(
() => ({ content: 'Done.' }),
FAKE_SERVER_OPTIONS,
);
try {
const q = query({
prompt: 'Hello',
options: {
...SHARED_TEST_OPTIONS,
...fakeModelOptions(fakeServer.baseUrl),
cwd: testDir,
},
});
await q.close();
await expect(q.supportedCommands()).rejects.toThrow('Query is closed');
} finally {
await fakeServer.close();
}
});
});
});

View file

@ -50,7 +50,7 @@
"test:integration:sandbox:none": "cross-env QWEN_SANDBOX=false vitest run --root ./integration-tests",
"test:integration:sandbox:docker": "cross-env QWEN_SANDBOX=docker npm run build:sandbox && QWEN_SANDBOX=docker vitest run --root ./integration-tests",
"test:integration:sandbox:podman": "cross-env QWEN_SANDBOX=podman vitest run --root ./integration-tests",
"test:integration:no-ak:sandbox:none": "cross-env QWEN_SANDBOX=false vitest run --root ./integration-tests ./fake-openai-server.test.ts ./cli/qwen-serve-routes.test.ts ./cli/qwen-serve-streaming.test.ts ./cli/daemon-invocation-context.test.ts",
"test:integration:no-ak:sandbox:none": "cross-env QWEN_SANDBOX=false vitest run --root ./integration-tests --maxWorkers 2 ./fake-openai-server.test.ts ./cli/daemon-invocation-context.test.ts ./cli/list_directory.test.ts ./cli/qwen-serve-routes.test.ts ./cli/qwen-serve-streaming.test.ts ./sdk-typescript/abort-and-lifecycle.test.ts ./sdk-typescript/permission-control.test.ts ./sdk-typescript/sdk-mcp-server.test.ts ./sdk-typescript/subagents.test.ts ./sdk-typescript/system-control.test.ts ./sdk-typescript/tool-control.test.ts",
"test:integration:sdk:sandbox:none": "cross-env QWEN_SANDBOX=false vitest run --root ./integration-tests --poolOptions.threads.maxThreads 2 sdk-typescript",
"test:integration:sdk:sandbox:docker": "cross-env QWEN_SANDBOX=docker npm run build:sandbox && QWEN_SANDBOX=docker vitest run --root ./integration-tests --poolOptions.threads.maxThreads 2 sdk-typescript",
"test:sdk:python": "python3 -m pytest -c packages/sdk-python/pyproject.toml packages/sdk-python/tests -q",

View file

@ -47,6 +47,9 @@ export {
// ---------------------------------------------------------------------------
/** All known providers, in display order. */
// Adding a provider that reads credentials from a new env key? Also add that
// key to the no-AK gate's cleared-env list in .github/workflows/ci.yml and to
// scripts/tests/no-ak-integration-ci.test.js, or the gate can leak runner creds.
export const ALL_PROVIDERS: readonly ProviderConfig[] = [
codingPlanProvider,
tokenPlanProvider,

View file

@ -33,16 +33,23 @@ describe('no-AK integration CI wiring', () => {
expect(packageJson.scripts[NO_AK_SCRIPT]).toBe(
[
'cross-env QWEN_SANDBOX=false vitest run --root ./integration-tests',
'cross-env QWEN_SANDBOX=false vitest run --root ./integration-tests --maxWorkers 2',
'./fake-openai-server.test.ts',
'./cli/daemon-invocation-context.test.ts',
'./cli/list_directory.test.ts',
'./cli/qwen-serve-routes.test.ts',
'./cli/qwen-serve-streaming.test.ts',
'./cli/daemon-invocation-context.test.ts',
'./sdk-typescript/abort-and-lifecycle.test.ts',
'./sdk-typescript/permission-control.test.ts',
'./sdk-typescript/sdk-mcp-server.test.ts',
'./sdk-typescript/subagents.test.ts',
'./sdk-typescript/system-control.test.ts',
'./sdk-typescript/tool-control.test.ts',
].join(' '),
);
});
it('runs the no-AK integration script in the Ubuntu gate only', () => {
it('runs the no-AK integration script in the required Linux gate only', () => {
const workflow = readFileSync(
path.join(ROOT, '.github/workflows/ci.yml'),
'utf8',
@ -50,13 +57,68 @@ describe('no-AK integration CI wiring', () => {
const ubuntuJob = getWorkflowJob(workflow, 'test');
const macosJob = getWorkflowJob(workflow, 'test_macos');
const windowsJob = getWorkflowJob(workflow, 'test_windows');
const permissionsIndex = workflow.indexOf('\npermissions:');
expect(permissionsIndex).toBeGreaterThan(0);
const workflowTriggers = workflow.slice(0, permissionsIndex);
const gateStepMarker =
" - name: 'Run required no-AK integration gate'";
const gateStepStart = ubuntuJob.indexOf(gateStepMarker);
expect(gateStepStart).toBeGreaterThanOrEqual(0);
const nextStepIndex = ubuntuJob.indexOf(
'\n - name:',
gateStepStart + gateStepMarker.length,
);
expect(nextStepIndex).toBeGreaterThan(0);
const gateStep = ubuntuJob.slice(gateStepStart, nextStepIndex);
expect(workflow).not.toContain(' integration_no_ak:');
expect(workflow.split(`npm run ${NO_AK_SCRIPT}`).length - 1).toBe(1);
expect(workflowTriggers).toContain('\n pull_request:\n');
expect(workflowTriggers).toContain('\n merge_group:\n');
expect(ubuntuJob).toContain("name: 'Run no-AK integration smoke tests'");
expect(ubuntuJob).toContain("github.event_name == 'pull_request'");
expect(ubuntuJob).toContain(`npm run ${NO_AK_SCRIPT}`);
expect(gateStep).toContain(
"(github.event_name == 'pull_request' || github.event_name == 'merge_group')",
);
expect(gateStep).toContain(`npm run ${NO_AK_SCRIPT}`);
expect(gateStep).toContain(
"QWEN_HOME: '${{ runner.temp }}/qwen-no-ak-home/.qwen'",
);
expect(gateStep).toContain('timeout-minutes: 20');
expect(gateStep).toContain(
"\n HOME: '${{ runner.temp }}/qwen-no-ak-home'",
);
expect(gateStep).toContain(
"\n USERPROFILE: '${{ runner.temp }}/qwen-no-ak-home'",
);
for (const key of [
'API_KEY',
'ANTHROPIC_API_KEY',
'ANTHROPIC_BASE_URL',
'ANTHROPIC_MODEL',
'BAILIAN_CODING_PLAN_API_KEY',
'BAILIAN_TOKEN_PLAN_API_KEY',
'DASHSCOPE_API_KEY',
'DEEPSEEK_API_KEY',
'GEMINI_API_KEY',
'GEMINI_MODEL',
'GOOGLE_API_KEY',
'GOOGLE_MODEL',
'IDEALAB_API_KEY',
'MINIMAX_API_KEY',
'MODELSCOPE_API_KEY',
'OPENAI_API_KEY',
'OPENAI_BASE_URL',
'OPENAI_MODEL',
'OPENROUTER_API_KEY',
'QWEN_API_KEY',
'QWEN_DEFAULT_AUTH_TYPE',
'QWEN_MODEL',
'REQUESTY_API_KEY',
'XAI_API_KEY',
'ZAI_API_KEY',
]) {
expect(gateStep).toContain(`\n ${key}: ''`);
}
expect(ubuntuJob).not.toContain('secrets.OPENAI_API_KEY');
expect(ubuntuJob).not.toContain('secrets.OPENAI_BASE_URL');
expect(ubuntuJob).not.toContain('secrets.OPENAI_MODEL');