Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
82 commits
Select commit Hold shift + click to select a range
9994b2c
🪚 fix: Restore Measured Width to Virtualized Model Lists (#16252)
lia-by-librechat[bot] Sep 23, 2026
b374360
🚡 chore: Bump Agents SDK to v3.9.3 (#16261)
lia-by-librechat[bot] Sep 23, 2026
3341e4d
🔐 ci: Gate the Legacy Mirror Behind an Environment (#16238)
danny-avila Sep 23, 2026
e4fe316
🚉 fix: Keep Queue Controls with the Starting Turn (#16268)
lia-by-librechat[bot] Sep 24, 2026
89effb9
🛝 refactor: Route Packaged Chat Completion Frames Through a Writer (#…
lia-by-librechat[bot] Sep 24, 2026
7979bdb
🔘 fix: Sync Workspace Choices When Chats Start (#16266)
lia-by-librechat[bot] Sep 24, 2026
b28a89e
🚸 fix: Scope Queued-Turn Deletion to Selected Conversations (#16270)
lia-by-librechat[bot] Sep 24, 2026
764a439
📏 fix: Constrain the Agent Selector to 480px and 10 Own Agents (#16256)
berry-13 Sep 24, 2026
59da552
🤖 fix: Accept GitHub App bot commit identities (#16253)
danny-avila Sep 24, 2026
f13b0ea
🚚 chore: Point Repository and Image References at LibreChat-AI (#16005)
danny-avila Sep 24, 2026
02492bd
🏚️ feat: Recover Coding Chats With Missing Workspaces (#16273)
lia-by-librechat[bot] Sep 24, 2026
3a3fbb7
🏝️ ci: Keep Canary Pull Requests on Canary (#16285)
lia-by-librechat[bot] Sep 24, 2026
6a58bf1
🎈 fix: Restore Bedrock GPT Context for Long Prompts (#16284)
lia-by-librechat[bot] Sep 24, 2026
290e37e
🐤 docs: Clarify Maintainer-Directed Canary Pull Requests (#16291)
lia-by-librechat[bot] Sep 24, 2026
8fb4056
🦺 fix: Skip Redundant MCP OAuth Probes and Teardown Cancellations (#1…
lia-by-librechat[bot] Sep 24, 2026
d0850e7
🪁 fix: Keep Workspace Discovery Reactive (#16296)
lia-by-librechat[bot] Sep 24, 2026
7d5cbc9
🌫️ feat: Add Bounded PII Text Transformation (#16295)
lia-by-librechat[bot] Sep 24, 2026
e6929f5
🪴 fix: Surface Persistent Memory Before First Save (#16301)
lia-by-librechat[bot] Sep 24, 2026
ffd3d1e
🔕 perf: Back Off Idle Durable Agent Recovery Polls (#16303)
lia-by-librechat[bot] Sep 24, 2026
0829709
🥞 fix: Keep Tool Glyphs Behind Approval Reviews (#16307)
lia-by-librechat[bot] Sep 24, 2026
04e637e
🦉 fix: Quiet Idle Subagent Discovery While Preserving Child Wakeups (…
lia-by-librechat[bot] Sep 24, 2026
56301c1
🪚 fix: Count Large Prompts in Bounded Tokenizer Chunks (#16305)
lia-by-librechat[bot] Sep 24, 2026
edddf92
🪀 fix: Preserve Coding Approval Mode on Background Returns (#16322)
lia-by-librechat[bot] Sep 24, 2026
3788fe0
🐢 fix: Back Off Waiting Completion Wake-ups and Keep Their Trace Out …
danny-avila Sep 25, 2026
402c19c
🧪 ci: Count System Instructions in Mock Model Usage (#16350)
danny-avila Sep 25, 2026
7238be3
⚡ fix: Deliver Waiting Completion Wake-ups the Moment They Are Ready …
danny-avila Sep 25, 2026
5862b3b
🛍️ fix: Discover Managed Agents in Marketplace Search (#16324)
lia-by-librechat[bot] Sep 25, 2026
dd21e7f
📣 fix: Surface MCP Credential Failures During Tool Loading (#16323)
lia-by-librechat[bot] Sep 25, 2026
c6282fa
🧷 fix: Wait for Long Dispatch Turns Before Giving Up Background Code …
danny-avila Sep 25, 2026
34938eb
📮 fix: Count Undelivered Background Results as Outstanding (#16343)
danny-avila Sep 25, 2026
fe8a993
🚥 fix: Pause Rejected Steer Recoveries (#16306)
lia-by-librechat[bot] Sep 25, 2026
163c1ad
🍱 fix: Carry Coding Approval Mode With Queued Turns (#16326)
lia-by-librechat[bot] Sep 25, 2026
8183959
🧷 fix: Anchor Background Results Onto Tool Calls a Resumed Turn Saved…
danny-avila Sep 25, 2026
3c08a7d
📦 chore: bump `@librechat/agents` SDK to v3.9.6 (#16354)
danny-avila Sep 25, 2026
24570b6
🐙 ci: Spread Playwright Coverage Across Isolated Shards (#16358)
lia-by-librechat[bot] Sep 25, 2026
e32e6b5
📋 feat: View and Stop Background Tasks From the Chat Header (#16348)
danny-avila Sep 25, 2026
7b2362d
🧲 feat: Attach or Leave a Workspace on a Saved Chat (#16124)
lia-by-librechat[bot] Sep 25, 2026
6d7ba72
🛶 chore: Bring Canary Up to Date With Dev
lia-librechat Sep 25, 2026
09c12b9
🛶 fix: Reconcile Canary Sync Checks
lia-librechat Sep 25, 2026
c4fbffb
🛶 test: Isolate Marketplace Retries and MCP OAuth Ownership
lia-librechat Sep 25, 2026
b9b82d7
🛶 style: Match CI Tailwind Formatting in Canary Sync
lia-librechat Sep 25, 2026
18cfb51
🐳 fix: Fail the Docker Build When the Frontend Build Fails (#16388)
danny-avila Sep 26, 2026
84e3ea9
🧗 fix: Keep Manual Compactions on the Active Branch (#16386)
lia-by-librechat[bot] Sep 26, 2026
048816a
📬 fix: Show Background Results Still Waiting for the Agent (#16363)
danny-avila Sep 26, 2026
24f950a
🛃 fix: Project Only Accepted Agents API Tool Calls (#16384)
lia-by-librechat[bot] Sep 26, 2026
a329e01
🧨 fix: Reject Invalid AES Credentials Before Startup (#16402)
lia-by-librechat[bot] Sep 27, 2026
19e0160
🚞 fix: Route Accepted Tool Calls Through the Live Agents API (#16401)
lia-by-librechat[bot] Sep 27, 2026
c606e50
🛬 fix: Settle Background Tool Results Before the Server Shuts Down (#…
danny-avila Sep 27, 2026
a047548
🪜 feat: Title an Open Activity Fold and Reach Failed Calls in One Cli…
danny-avila Sep 27, 2026
cd405e6
📐 style: Set the Reasoning Header at the Shared Row Scale (#16391)
danny-avila Sep 27, 2026
b04226e
🫸 fix: Right-Align User Message Images (#16404)
lia-by-librechat[bot] Sep 27, 2026
269e6b3
💬 fix: Show a Thought on the Live Header Only as Finished Sentences (…
danny-avila Sep 27, 2026
d7209ef
🏷️ fix: Title an Open Live Card by Its Newest Label, Not a Line of It…
danny-avila Sep 27, 2026
9ffcd28
🫂 fix: Keep Legacy Background Results Until Durable (#16405)
lia-by-librechat[bot] Sep 27, 2026
0c47ce9
💭 feat: Bring the Streaming Thought Peek Back Under a Collapsed Live …
danny-avila Sep 27, 2026
7caa98f
🖊️ fix: Persist Skill Subfile Edits (#16362)
lia-by-librechat[bot] Sep 27, 2026
f15c8a0
🗂️ fix: Stop Telling Attached Workspaces That Uploads Are at /mnt/dat…
danny-avila Sep 27, 2026
3c72c3f
🐢 perf: Collapse Settled Reasoning Text in AnimatedText (#16403)
berry-13 Sep 27, 2026
2278da1
🚩 fix: Preserve Client Tool Call Finish Reason After Text (#16408)
lia-by-librechat[bot] Sep 27, 2026
26d5d01
🪁 fix: Prioritize Running Chats in Sidebar History (#16406)
lia-by-librechat[bot] Sep 27, 2026
2068a8b
🏺 fix: Preserve Last Good Config on Reload (#16383)
lia-by-librechat[bot] Sep 27, 2026
696ebed
🎴 feat: Render Background Tasks as Native Tool Cards (#16407)
lia-by-librechat[bot] Sep 27, 2026
241c692
🛶 chore: Include Latest Canary Work in Sync
lia-librechat Sep 27, 2026
6113242
🛶 chore: Merge Latest Dev into Canary Sync
lia-librechat Sep 27, 2026
f510bd7
🛶 fix: Reconcile Canary Server Startup and Static Gates
lia-librechat Sep 27, 2026
129a1de
📏 fix: Keep the Context Gauge's Used Count Above Its Own Breakdown (#…
danny-avila Sep 27, 2026
3c8a570
🛶 chore: Sync Latest Dev Context Gauge into Canary
lia-librechat Sep 27, 2026
da5e393
🏷️ fix: Title an Open Live Card Only by a Generated Label (#16412)
danny-avila Sep 27, 2026
d297530
📟 refactor: Present Background Task Checks as a Distinct Activity (#1…
lia-by-librechat[bot] Sep 27, 2026
a45be88
🛶 chore: Include Latest Canary Chat Facade in Sync
lia-librechat Sep 27, 2026
3789cbf
🛶 chore: Include Latest Dev Activity Updates in Canary Sync
lia-librechat Sep 27, 2026
f7f7567
🫖 fix: Keep Sidebar Search Results Visible During Refetch (#16411)
lia-by-librechat[bot] Sep 27, 2026
8ec632f
📡 fix: Explain Model Streams That Die Mid-Response (#16415)
danny-avila Sep 27, 2026
bf91d2b
🥧 fix: Reserve Execution Time in Workspace Admission (#16421)
lia-by-librechat[bot] Sep 28, 2026
6658e3b
📴 fix: Surface Missing Scheduled OBO Authorization (#16416)
lia-by-librechat[bot] Sep 28, 2026
81e0389
🛶 chore: Include Latest Canary Features in Sync
lia-librechat Sep 28, 2026
c505db2
🛶 chore: Merge Latest Dev Fixes into Canary Sync
lia-librechat Sep 28, 2026
3c7169c
🛶 style: Match Schedule Recovery Theme Formatting
lia-librechat Sep 28, 2026
c8c5478
📦 chore: bump agents SDK to v3.9.7 (#16425)
danny-avila Sep 28, 2026
25d74a6
🛶 fix: Keep Revealed Queue Actions and E2E Flows After Composer Redesign
lia-librechat Sep 28, 2026
420760d
🛶 chore: Include Latest Agents SDK Bump in Canary Sync
lia-librechat Sep 28, 2026
66a6e63
🛶 fix: Remove Obsolete Steer Recovery Translation Keys
lia-librechat Sep 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
6 changes: 3 additions & 3 deletions .github/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Thank you to all the contributors who have helped make this project possible! We

## Contributing Guidelines

If the feature you would like to contribute has not already received prior approval from the project maintainers (i.e., the feature is currently on the [roadmap](https://github.com/users/danny-avila/projects/2)), please submit a request in the [Feature Requests & Suggestions category](https://github.com/danny-avila/LibreChat/discussions/new?category=feature-requests-suggestions) of the discussions board before beginning work on it. The requests should include specific implementation details, including areas of the application that will be affected by the change (including designs if applicable), and any other relevant information that might be required for a speedy review. However, proposals are not required for small changes, bug fixes, or documentation improvements. Small changes and bug fixes should be tied to an [issue](https://github.com/danny-avila/LibreChat/issues) and included in the corresponding pull request for tracking purposes.
If the feature you would like to contribute has not already received prior approval from the project maintainers (i.e., the feature is currently on the [roadmap](https://github.com/users/danny-avila/projects/2)), please submit a request in the [Feature Requests & Suggestions category](https://github.com/LibreChat-AI/LibreChat/discussions/new?category=feature-requests-suggestions) of the discussions board before beginning work on it. The requests should include specific implementation details, including areas of the application that will be affected by the change (including designs if applicable), and any other relevant information that might be required for a speedy review. However, proposals are not required for small changes, bug fixes, or documentation improvements. Small changes and bug fixes should be tied to an [issue](https://github.com/LibreChat-AI/LibreChat/issues) and included in the corresponding pull request for tracking purposes.

Please note that a pull request involving a feature that has not been reviewed and approved by the project maintainers may be rejected. We appreciate your understanding and cooperation.

Expand All @@ -26,7 +26,7 @@ A pull request that appears unannounced, with no issue, no assignment and no pri

**The one exception is a novel P0/P1 defect**: data loss, a broken release, a crash, or a regression with no workaround, that nobody has reported yet. Open it, and put the impact and the reproduction in the first paragraph. Novel is the operative word. A patch for something already reported, already assigned, or already fixed on `dev` is not an exception, and neither is a cosmetic or speculative change dressed up as urgent.

**Security is never an exception.** Do not open a pull request, an issue, or a public message that describes a vulnerability, even a critical one, and even with a fix attached. A pull request is a public disclosure that explains the attack and points at the affected code. Report it through LibreChat's [private vulnerability reporting form](https://github.com/danny-avila/LibreChat/security/advisories/new) and we will open a private channel and coordinate the fix and its release there. See [SECURITY.md](./SECURITY.md).
**Security is never an exception.** Do not open a pull request, an issue, or a public message that describes a vulnerability, even a critical one, and even with a fix attached. A pull request is a public disclosure that explains the attack and points at the affected code. Report it through LibreChat's [private vulnerability reporting form](https://github.com/LibreChat-AI/LibreChat/security/advisories/new) and we will open a private channel and coordinate the fix and its release there. See [SECURITY.md](./SECURITY.md).

### Pull requests generated from issues

Expand Down Expand Up @@ -82,7 +82,7 @@ Project maintainers have the right and responsibility to remove, edit, or reject

1. Before starting work, sync `dev` from this repository. You are working in a fork, so `origin` is
your fork — add the canonical remote once and sync from it:
- `git remote add upstream https://github.com/danny-avila/LibreChat.git`
- `git remote add upstream https://github.com/LibreChat-AI/LibreChat.git`
- `git fetch upstream dev && git checkout -B dev upstream/dev`
- `npm run update` is the self-host deployment updater — it checks out `main` and rebuilds your
containers. Do not use it to refresh a development branch.
Expand Down
6 changes: 3 additions & 3 deletions .github/ISSUE_TEMPLATE/BUG-REPORT.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ body:
Thanks for taking the time to fill out this bug report!
Before submitting, please:
- Search existing [Issues and Discussions](https://github.com/danny-avila/LibreChat/discussions) to see if your bug has already been reported
- Use [Discussions](https://github.com/danny-avila/LibreChat/discussions) instead of Issues for:
- Search existing [Issues and Discussions](https://github.com/LibreChat-AI/LibreChat/discussions) to see if your bug has already been reported
- Use [Discussions](https://github.com/LibreChat-AI/LibreChat/discussions) instead of Issues for:
- General inquiries
- Help with setup
- Questions about whether you're experiencing a bug
Expand Down Expand Up @@ -87,7 +87,7 @@ body:
id: terms
attributes:
label: Code of Conduct
description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/danny-avila/LibreChat/blob/main/.github/CODE_OF_CONDUCT.md)
description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/LibreChat-AI/LibreChat/blob/main/.github/CODE_OF_CONDUCT.md)
options:
- label: I agree to follow this project's Code of Conduct
required: true
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/FEATURE-REQUEST.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ body:
id: terms
attributes:
label: Code of Conduct
description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/danny-avila/LibreChat/blob/main/.github/CODE_OF_CONDUCT.md)
description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/LibreChat-AI/LibreChat/blob/main/.github/CODE_OF_CONDUCT.md)
options:
- label: I agree to follow this project's Code of Conduct
required: true
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/NEW-LANGUAGE-REQUEST.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ body:
id: terms
attributes:
label: Code of Conduct
description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/danny-avila/LibreChat/blob/main/.github/CODE_OF_CONDUCT.md).
description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/LibreChat-AI/LibreChat/blob/main/.github/CODE_OF_CONDUCT.md).
options:
- label: I agree to follow this project's Code of Conduct
required: true
4 changes: 2 additions & 2 deletions .github/SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ At LibreChat, we prioritize the security of our project and value the contributi

When reporting a security vulnerability, you have the following options to reach out to us:

- **Option 1: GitHub Private Vulnerability Reporting**: Submit sensitive vulnerability details through LibreChat's [private vulnerability reporting form](https://github.com/danny-avila/LibreChat/security/advisories/new). This sends the report confidentially to the project maintainers.
- **Option 1: GitHub Private Vulnerability Reporting**: Submit sensitive vulnerability details through LibreChat's [private vulnerability reporting form](https://github.com/LibreChat-AI/LibreChat/security/advisories/new). This sends the report confidentially to the project maintainers.

- **Option 2: GitHub Issues**: You can initiate first contact via GitHub Issues. However, please note that initial contact through GitHub Issues should not include any sensitive details.

Expand Down Expand Up @@ -43,7 +43,7 @@ This security policy applies to the following GitHub repository:

## Contact

If you have any questions or concerns regarding the security of our project, please join our [Discord community](https://discord.librechat.ai) and report them in the appropriate channel. You can also reach out to us by [opening an issue](https://github.com/danny-avila/LibreChat/issues/new) on GitHub. Please note that the response time may vary depending on the nature and severity of the inquiry.
If you have any questions or concerns regarding the security of our project, please join our [Discord community](https://discord.librechat.ai) and report them in the appropriate channel. You can also reach out to us by [opening an issue](https://github.com/LibreChat-AI/LibreChat/issues/new) on GitHub. Please note that the response time may vary depending on the nature and severity of the inquiry.

## Acknowledgments

Expand Down
4 changes: 2 additions & 2 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Pull Request

> Before submitting, please review the [Contributing Guide](https://github.com/danny-avila/LibreChat/blob/main/.github/CONTRIBUTING.md).
> Before submitting, please review the [Contributing Guide](https://github.com/LibreChat-AI/LibreChat/blob/main/.github/CONTRIBUTING.md).
>
> Documentation changes belong in the [LibreChat documentation repository](https://github.com/LibreChat-AI/librechat.ai).

Expand All @@ -26,7 +26,7 @@ introduced a regression is the exception: that is history the reader needs.
Link related issues, and name any dependency this change needs:
Fixes #123
Related to #123
Depends on danny-avila/agents#123
Depends on LibreChat-AI/agents#123
-->

## How it works
Expand Down
211 changes: 211 additions & 0 deletions .github/scripts/playwright-matrix.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
import yaml from 'js-yaml';
import { test } from 'node:test';
import { join } from 'node:path';
import assert from 'node:assert/strict';
import { fileURLToPath } from 'node:url';
import { spawnSync } from 'node:child_process';
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';

const scriptsDir = fileURLToPath(new URL('.', import.meta.url));
const workflow = yaml.load(
readFileSync(new URL('../workflows/playwright-mock.yml', import.meta.url), 'utf8'),
);
const selector = workflow.jobs.codegraph_select.steps.find((step) => step.id === 'sel');
const shards = workflow.jobs.e2e_shards;
const defaults = [...shards.strategy.matrix.matchAll(/fromJSON\('([^']+)'\)/g)].map((match) =>
JSON.parse(match[1]),
);
const prMatrix = JSON.parse(selector.env.FULL_INCLUDE);

function assertPartition(matrix, store, suite, count) {
const lanes = matrix.include.filter(
(lane) => lane.stream_store === store && lane.suite === suite,
);
assert.equal(lanes.length, count);
assert.deepEqual(
lanes.map((lane) => lane.shard),
Array.from({ length: count }, (_, index) => `${index + 1}/${count}`),
);
assert.ok(
lanes.every((lane) => lane.redis_image === (store === 'redis' ? 'redis:7-alpine' : '')),
);
}

function select(t, response, overrides = {}) {
const scratch = mkdtempSync(join(scriptsDir, '.playwright-matrix-test-'));
t.after(() => rmSync(scratch, { recursive: true, force: true }));
writeFileSync(join(scratch, 'response.json'), JSON.stringify(response));
writeFileSync(join(scratch, 'output'), '');
writeFileSync(join(scratch, 'selector.sh'), selector.run);
writeFileSync(
join(scratch, 'gh'),
'#!/usr/bin/env bash\nprintf \'%s\\n\' \'{"path":"client/src/example.ts","status":"modified"}\'\nexit "${GH_EXIT:-0}"\n',
{ mode: 0o700 },
);
writeFileSync(
join(scratch, 'curl'),
'#!/usr/bin/env bash\ncat response.json\nexit "${CURL_EXIT:-0}"\n',
{ mode: 0o700 },
);
const result = spawnSync('bash', ['selector.sh'], {
cwd: scratch,
encoding: 'utf8',
timeout: 10000,
env: {
PATH: `${scratch}:${process.env.PATH}`,
FULL_INCLUDE: selector.env.FULL_INCLUDE,
URL: 'https://selector.invalid',
TOKEN: 'test-fixture',
REPO: 'fixture/repo',
PR: '1',
BASE_SHA: 'base',
HEAD_SHA: 'head',
CHANGED: '1',
E2E_SKIP_ARMED: '',
GITHUB_OUTPUT: join(scratch, 'output'),
GITHUB_STEP_SUMMARY: join(scratch, 'summary'),
...overrides,
},
});
assert.equal(result.status, 0, result.stdout + result.stderr);
const outputs = Object.fromEntries(
readFileSync(join(scratch, 'output'), 'utf8')
.trim()
.split('\n')
.filter(Boolean)
.map((line) => {
const separator = line.indexOf('=');
return [line.slice(0, separator), line.slice(separator + 1)];
}),
);
return { outputs, matrix: outputs.e2e_include ? JSON.parse(outputs.e2e_include) : defaults[0] };
}

function decision(redis, mcp = true) {
return { matrix: { 'playwright-mock': { redis_transport: redis, mcp_tool_list_changed: mcp } } };
}

test('PR selection and fail-open defaults use six memory shards and two Redis transport shards', () => {
assert.equal(defaults.length, 2);
assert.deepEqual(defaults[0], prMatrix);
assert.equal(prMatrix.include.length, 8);
assertPartition(prMatrix, 'memory', 'full', 6);
assertPartition(prMatrix, 'redis', 'transport', 2);
});

test('nightly and default manual runs retain full-suite coverage on both stores', () => {
assert.equal(workflow.on.workflow_dispatch.inputs.coverage.default, 'full');
assert.equal(defaults[1].include.length, 4);
assertPartition(defaults[1], 'memory', 'full', 2);
assertPartition(defaults[1], 'redis', 'full', 2);
});

test('PR and nightly matrices preserve both registered-email change modes on one memory shard', () => {
for (const matrix of [prMatrix, ...defaults]) {
const emailLanes = matrix.include.filter((lane) => lane.email_change === 'true');
assert.equal(emailLanes.length, 1);
assert.equal(emailLanes[0].stream_store, 'memory');
assert.match(emailLanes[0].shard, /^1\//);
}
assert.equal(shards.steps.filter((step) => step.if === "matrix.email_change == 'true'").length, 2);
});

test('manual PR coverage opts into the PR fallback matrix without bypassing the PR author gate', () => {
assert.deepEqual(workflow.on.workflow_dispatch.inputs.coverage.options, ['full', 'pr']);
assert.match(
shards.strategy.matrix,
/github\.event_name == 'pull_request' \|\| inputs\.coverage == 'pr'/,
);
assert.match(shards.if, /github\.event_name == 'workflow_dispatch'/);
assert.match(shards.if, /github\.event\.pull_request\.author_association/);
});

test('every shard has a unique job name and artifact namespace', () => {
for (const matrix of [prMatrix, ...defaults]) {
for (const key of ['name', 'artifact']) {
assert.equal(new Set(matrix.include.map((lane) => lane[key])).size, matrix.include.length);
}
}
});

test('both suite commands consume the matrix shard without increasing worker concurrency', () => {
for (const suite of ['full', 'transport']) {
const step = shards.steps.find((entry) => entry.if === `matrix.suite == '${suite}'`);
assert.match(step.run, /--shard=\$\{\{ matrix\.shard \}\}/);
assert.doesNotMatch(step.run, /--workers/);
}
const config = readFileSync(
new URL('../../e2e/playwright.config.mock.ts', import.meta.url),
'utf8',
);
assert.match(config, /workers: 1,/);
assert.equal(shards['runs-on'], 'ubuntu-latest');
});

test('an explicit Redis skip drops both transport shards and no memory shard', (t) => {
const { matrix, outputs } = select(t, decision(false, false));
assert.equal(outputs.decided, 'true');
assert.equal(outputs.mcp_run, 'false');
assert.equal(matrix.include.length, 6);
assertPartition(matrix, 'memory', 'full', 6);
});

test('missing or non-boolean Redis decisions retain both transport shards', (t) => {
for (const redis of [true, 'false', null, undefined]) {
const { matrix } = select(t, decision(redis));
assert.deepEqual(matrix, prMatrix);
assertPartition(matrix, 'redis', 'transport', 2);
}
});

test('unavailable configuration, failed requests, and incomplete file lists fall back to the full PR matrix', (t) => {
for (const overrides of [
{ URL: '' },
{ TOKEN: '' },
{ CURL_EXIT: '28' },
{ GH_EXIT: '1' },
{ CHANGED: '2' },
]) {
const { matrix, outputs } = select(t, decision(false, false), overrides);
assert.equal(outputs.e2e_include, undefined);
assert.equal(outputs.mcp_run, undefined);
assert.deepEqual(matrix, prMatrix);
}
const { matrix, outputs } = select(t, {});
assert.equal(outputs.e2e_include, undefined);
assert.deepEqual(matrix, prMatrix);
});

test('graduated spec skips keep the same pool across shards and remain opt-in', (t) => {
const response = {
...decision(true),
e2e: { fail_open: false, graduated: ['e2e/specs/mock/sidebar.spec.ts'] },
};
const dark = select(t, response);
assert.equal(dark.outputs.e2e_skip, '');
const armed = select(t, response, { E2E_SKIP_ARMED: 'on' });
assert.equal(armed.outputs.e2e_skip, 'specs/mock/sidebar.spec.ts');
assert.deepEqual(armed.matrix, prMatrix);
const fallback = select(
t,
{ ...response, e2e: { ...response.e2e, fail_open: true } },
{ E2E_SKIP_ARMED: 'on' },
);
assert.equal(fallback.outputs.e2e_skip, '');
assert.deepEqual(fallback.matrix, prMatrix);
});

test('the required gate still waits for all shards and rejects failures without fighting cancellation', () => {
const gate = workflow.jobs.e2e;
assert.equal(gate.name, 'e2e');
assert.deepEqual(gate.needs, ['codegraph_select', 'e2e_shards', 'mcp_tool_list_changed', 'mcp_apps']);
assert.equal(shards.strategy['fail-fast'], false);
assert.match(gate.if, /!cancelled\(\)/);
assert.match(shards.if, /!cancelled\(\)/);
assert.match(gate.steps[0].if, /needs\.e2e_shards\.result != 'success'/);
assert.match(gate.steps[0].if, /needs\.mcp_apps\.result != 'success'/);
assert.match(gate.steps[0].if, /needs\.mcp_tool_list_changed\.result != 'success'/);
assert.match(gate.steps[0].if, /needs\.codegraph_select\.outputs\.mcp_run == 'false'/);
assert.equal(gate.steps[0].run, 'exit 1');
assert.equal(workflow.concurrency['cancel-in-progress'], true);
});
8 changes: 8 additions & 0 deletions .github/scripts/retarget-prs.sh
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,14 @@ for number in "$@"; do
continue
fi

# Canary is an intentional integration target. Keep it untouched even if a caller passes
# its number to the sweep or overrides RELEASE_BASE, TARGET_BASE or EXPLAIN_MISSING.
if [ "$base_ref" = "canary" ]; then
echo "#$number: skipped — canary is an explicit target"
skipped=$((skipped + 1))
continue
fi

if [ "$base_ref" = "$TARGET_BASE" ] && [ "$EXPLAIN_MISSING" = "true" ] && [ "$DRY_RUN" != "true" ]; then
echo "#$number: already on $TARGET_BASE — posting any missing explanation"
post_explanation "$number"
Expand Down
Loading
Loading