Skip to content

Commit f37f13a

Browse files
atulmguptaCopilot
andauthored
refactor(ai): first-class Microsoft Foundry v1 protocols (#123)
* fix(ai): stop forcing Foundry v1 from hostname; retry classic chat on 404 Helix Chat 404 DeploymentNotFound after #118 because every *.services.ai.azure.com host was routed to /openai/v1/chat/completions without api-version. Restore flavor deployments URLs unless the path explicitly contains /openai/v1, and fall back to that classic URL when v1 chat 404s. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 5b104204-65b7-4d04-90a4-e2d5897b8e2e * fix(ai): give gpt-5 Azure chats a real completion budget Helix dispatch leaves MaxTokens unset. Azure gpt-5 then 400s with 'max_tokens or model output limit was reached' because reasoning tokens consume the tiny default. Send max_completion_tokens=8192 for gpt-5/o-series (and Foundry v1) instead of max_tokens. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 5b104204-65b7-4d04-90a4-e2d5897b8e2e * fix(ai): use Foundry Responses API for gpt-5.6-sol Official Microsoft Foundry docs route gpt-5.6-sol through POST /openai/v1/responses (not chat/completions). Chat Completions rejects function tools on gpt-5.6+ unless reasoning_effort=none; Helix uses tools. Encode input/instructions/max_output_tokens, omit temperature, and keep classic chat completions for gpt-4o/gpt-5. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 5b104204-65b7-4d04-90a4-e2d5897b8e2e * fix(ai): route Foundry by surface, not a hardcoded model Responses API is used for every Microsoft Foundry flavor and every /openai/v1 endpoint, regardless of deployment name. Classic Azure OpenAI flavor without that path stays on chat completions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 5b104204-65b7-4d04-90a4-e2d5897b8e2e * fix(ai): negotiate Azure chat and Responses without model binding Use bounded structured-error fallback on explicit v1 endpoints, preserve legacy inference and both failures, align validation with Helix deployment identity, and honor caller token budgets. Disable Responses storage and reject unsuccessful results. Cover HTTP and real dispatch tool roundtrips plus settings persistence. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 5b104204-65b7-4d04-90a4-e2d5897b8e2e * fix(ai): sharpen alert template creativity and evaluation Replace token-checklist copy with event-specific creative direction and state-aware safety constraints. Align evaluation with the production prompt and add the reported reverse-gear regression. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 5b104204-65b7-4d04-90a4-e2d5897b8e2e * fix(web): avoid unsaved warning for untouched recovered alert drafts Use the hydrated editor as the navigation baseline, reset it on draft discard, and verify navigation for pristine, restored, edited, reverted, and saved-rule editors. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 5b104204-65b7-4d04-90a4-e2d5897b8e2e * refactor(ai): make Microsoft Foundry v1 the only Azure surface Remove classic deployment and inference routes, API versions, flavors, override fields and model-name heuristics. Add explicit Auto, Chat Completions and Responses protocol selection across settings, validation and dispatch. Keep azure credentials identity and migrate effective saved deployment names at the config boundary. Embeddings always use Foundry v1. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 5b104204-65b7-4d04-90a4-e2d5897b8e2e * feat(alerts): add curated packs and Helix custom groups Add reviewed atomic pack installation, duplicate reuse, safe removal, ordinary-rule editing, opt-in proposal-only Helix support, and lifecycle regression tests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 5b104204-65b7-4d04-90a4-e2d5897b8e2e * fix(alerts): use canonical seconds for pack cooldown requests Keep the existing ordinary-rule boundary unchanged and clone the migrated schema into isolated integration tests instead of redeclaring legacy columns. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 5b104204-65b7-4d04-90a4-e2d5897b8e2e * fix(ci): classify alert pack configuration mutations Pack installation and removal follow the mode-independent policy of ordinary alert-rule configuration, without issuing vehicle commands. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 5b104204-65b7-4d04-90a4-e2d5897b8e2e * fix(web): organize settings into responsive readable categories Preserve all preference forms and drafts across category changes, retain search and deep links, group general preferences, isolate reset controls, and keep action text readable across display sizes. Cover six widths in both themes and update reviewed settings visual baselines. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 5b104204-65b7-4d04-90a4-e2d5897b8e2e * fix(ai): preserve Foundry continuation and terminal semantics Bound dispatcher retries, reject incomplete tool output, preserve cancellation, and carry stateless Responses reasoning through tool turns. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 5b104204-65b7-4d04-90a4-e2d5897b8e2e * docs: fix get-started card links Remove the obsolete `/teslasync` base path so documentation cards resolve correctly. * fix(web): keep settings tour readable before translations initialize Provide explicit bootstrap copy without initializing i18n from the tour registry; verify both uninitialized and translated states. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 5b104204-65b7-4d04-90a4-e2d5897b8e2e * feat(notifications): deepen alert packs and streamline rule management Add master and per-rule delivery controls, 70 supported templates across 14 packs, persistent channel routing, and one contextual bulk-action toolbar. Keep empty lists uncluttered and verify responsive Helix and Studio flows. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 5b104204-65b7-4d04-90a4-e2d5897b8e2e * refactor(web): redesign alert pack preview as a responsive workspace Use a desktop comparison table and mobile cards, progressive rule editing, master defaults sidebar, filtered selection, and persistent installation actions. Preserve configuration across pagination and responsive layout changes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 5b104204-65b7-4d04-90a4-e2d5897b8e2e * ci: parallelize test suites with verified coverage merging Shard Go race and Vitest suites across eight runners each, split independent checks and builds, and reuse a single browser build across isolated browser and visual shards. Preserve required aggregate checks and reject incomplete coverage artifacts. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 5b104204-65b7-4d04-90a4-e2d5897b8e2e * test(ci): enforce shared browser build and preview lifecycle Update the E2E harness contract for artifact-backed browser shards using parsed YAML rather than adjacent job-name slicing. Assert a single hermetic build, mandatory artifact reuse, and managed preview wrappers for every suite. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 5b104204-65b7-4d04-90a4-e2d5897b8e2e * ci: gate Docker builds on successful test and coverage checks Keep test shards and validation jobs parallel, but start image builds only after generated-artifact, backend, and frontend gates succeed to avoid wasting build compute on failed tests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 5b104204-65b7-4d04-90a4-e2d5897b8e2e * feat(web): edit alert pack rules inline with per-message Helix Expose triggers, delivery overrides, channels and notification messages directly in pack rows and mobile cards. Validate channel selections once before atomic installation and preserve explicit suggestion review and apply. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 5b104204-65b7-4d04-90a4-e2d5897b8e2e * test(web): guard Settings readability across display and text sizes Verify 320-2560px layouts in light and dark themes at normal and 135% text scale. Check shortcut text width, action placement and hit targets, category description separation, unclipped overview text and preserved drafts. Capture individual shortcut screenshots for inspection. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 5b104204-65b7-4d04-90a4-e2d5897b8e2e * fix(web): wrap Settings summary values at larger text sizes Allow summary cards to shrink and wrap long values instead of overflowing with Linux system fonts at 135% text scale. Keep the strengthened responsive assertions. Exclude native footer elements from synthetic long-copy mutation, matching the existing contentinfo exclusion, and refresh the two inspected long-content baselines without changing screenshot thresholds. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 5b104204-65b7-4d04-90a4-e2d5897b8e2e * fix(web): flatten alert pack editing into aligned grid columns Use one property per column and consistently sized editors. Flatten pack-default controls, show effective once/repeat values without a Master option, and keep field-specific overrides with an explicit reset action. Preserve Helix, channel routing, mobile editing, validation and installation behavior. Add measured alignment and row-density regressions across eight viewport widths. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 5b104204-65b7-4d04-90a4-e2d5897b8e2e * feat(web): add pack channel defaults and consistent alert options Inherit pack channels unless a rule explicitly overrides them; reset channels with other delivery fields. Reuse Studio behavior labels and the 15-minute cooldown while preserving responsive aligned controls. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 5b104204-65b7-4d04-90a4-e2d5897b8e2e --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 5b104204-65b7-4d04-90a4-e2d5897b8e2e
1 parent a0fa26e commit f37f13a

153 files changed

Lines changed: 10181 additions & 2127 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { readFileSync, readdirSync } from 'node:fs';
2+
import { createRequire } from 'node:module';
3+
import { join, resolve } from 'node:path';
4+
import { fileURLToPath } from 'node:url';
5+
6+
// Decode with Vitest's own dependency, including when npm does not hoist it.
7+
const requireWeb = createRequire(new URL('../../web/package.json', import.meta.url));
8+
const requireVitest = createRequire(requireWeb.resolve('vitest/package.json'));
9+
const { parse } = requireVitest('flatted');
10+
11+
export function checkBlobs(directory, count, expectedFiles) {
12+
if (!Number.isInteger(count) || count < 1) throw new Error('Invalid shard count');
13+
const expectedNames = Array.from({ length: count }, (_, i) => `blob-${i + 1}-${count}.json`).sort();
14+
if (JSON.stringify(readdirSync(directory).sort()) !== JSON.stringify(expectedNames)) {
15+
throw new Error('Missing or unexpected Vitest shard artifacts');
16+
}
17+
const files = new Set();
18+
for (const name of expectedNames) {
19+
const [version, modules, errors, coverage] = parse(readFileSync(join(directory, name), 'utf8'));
20+
if (!version || !Array.isArray(modules) || modules.length === 0
21+
|| !Array.isArray(errors) || errors.length > 0
22+
|| !coverage || Object.keys(coverage).length === 0) {
23+
throw new Error(`${name}: missing tests, missing coverage, or unhandled errors`);
24+
}
25+
for (const module of modules) {
26+
if (typeof module.filepath !== 'string' || files.has(module.filepath)) {
27+
throw new Error(`${name}: invalid or duplicated test file`);
28+
}
29+
files.add(module.filepath);
30+
}
31+
}
32+
const expected = new Set(expectedFiles);
33+
if (files.size !== expected.size || [...expected].some(file => !files.has(file))) {
34+
throw new Error('Shard test files differ from full Vitest discovery');
35+
}
36+
console.log(`Validated ${count} Vitest shards: ${files.size} test files, no gaps or overlaps`);
37+
}
38+
39+
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
40+
const [, , directory, count, discovery] = process.argv;
41+
const files = JSON.parse(readFileSync(discovery, 'utf8')).map(entry => entry.file);
42+
checkBlobs(directory, Number(count), files);
43+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import assert from 'node:assert/strict';
2+
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
3+
import { createRequire } from 'node:module';
4+
import { tmpdir } from 'node:os';
5+
import { join } from 'node:path';
6+
import { afterEach, beforeEach, test } from 'node:test';
7+
import { checkBlobs } from './check_vitest_blobs.mjs';
8+
9+
const requireWeb = createRequire(new URL('../../web/package.json', import.meta.url));
10+
const { stringify } = createRequire(requireWeb.resolve('vitest/package.json'))('flatted');
11+
let root;
12+
const blob = (files, coverage = { 'source.ts': {} }, errors = []) =>
13+
stringify(['4.1.2', files.map(filepath => ({ filepath })), errors, coverage]);
14+
15+
beforeEach(() => {
16+
root = mkdtempSync(join(tmpdir(), 'vitest-shards-'));
17+
writeFileSync(join(root, 'blob-1-2.json'), blob(['/a.test.ts']));
18+
writeFileSync(join(root, 'blob-2-2.json'), blob(['/b.test.ts']));
19+
});
20+
afterEach(() => rmSync(root, { recursive: true, force: true }));
21+
22+
test('accepts exact discovery across all shards regardless of order', () => {
23+
checkBlobs(root, 2, ['/b.test.ts', '/a.test.ts']);
24+
});
25+
test('rejects missing and extraneous artifacts', () => {
26+
assert.throws(() => checkBlobs(root, 3, []), /artifacts/);
27+
writeFileSync(join(root, 'extra.json'), blob(['/c.test.ts']));
28+
assert.throws(() => checkBlobs(root, 2, []), /artifacts/);
29+
});
30+
test('rejects duplicated test files even if the artifact count is correct', () => {
31+
writeFileSync(join(root, 'blob-2-2.json'), blob(['/a.test.ts']));
32+
assert.throws(() => checkBlobs(root, 2, ['/a.test.ts', '/b.test.ts']), /duplicated/);
33+
});
34+
test('rejects omitted and unexpected tests', () => {
35+
for (const expected of [['/a.test.ts'], ['/a.test.ts', '/b.test.ts', '/c.test.ts']]) {
36+
assert.throws(() => checkBlobs(root, 2, expected), /discovery/);
37+
}
38+
});
39+
test('rejects empty tests, missing coverage, and unhandled worker errors', () => {
40+
for (const value of [blob([]), blob(['/b.test.ts'], null), blob(['/b.test.ts'], {}),
41+
blob(['/b.test.ts'], { 'source.ts': {} }, [{ message: 'worker crashed' }])]) {
42+
writeFileSync(join(root, 'blob-2-2.json'), value);
43+
assert.throws(() => checkBlobs(root, 2, ['/a.test.ts', '/b.test.ts']), /missing tests/);
44+
}
45+
});
46+
test('rejects corrupt reports and invalid shard counts', () => {
47+
writeFileSync(join(root, 'blob-2-2.json'), '{broken');
48+
assert.throws(() => checkBlobs(root, 2, []), SyntaxError);
49+
for (const count of [0, -1, 1.5, NaN]) {
50+
assert.throws(() => checkBlobs(root, count, []), /count/);
51+
}
52+
});

.github/scripts/ci_test_shards.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
#!/usr/bin/env python3
2+
"""Partition every Go package and merge complete, validated race/coverage shards."""
3+
4+
import argparse
5+
import json
6+
import re
7+
import subprocess
8+
from pathlib import Path
9+
10+
11+
def discover():
12+
output = subprocess.check_output(["go", "list", "-race", "-json", "./..."], text=True, encoding="utf-8")
13+
decoder = json.JSONDecoder()
14+
packages = []
15+
while output.strip():
16+
package, end = decoder.raw_decode(output.lstrip())
17+
output = output.lstrip()[end:]
18+
files = package.get("TestGoFiles", []) + package.get("XTestGoFiles", [])
19+
weight = 1 + sum((Path(package["Dir"]) / name).stat().st_size for name in files)
20+
packages.append((package["ImportPath"], weight))
21+
return packages
22+
23+
24+
def partition(packages, count):
25+
names = [name for name, _ in packages]
26+
if count < 1 or count > len(names) or len(set(names)) != len(names):
27+
raise ValueError("shards require distinct packages and a nonempty partition")
28+
shards = [[] for _ in range(count)]
29+
weights = [0] * count
30+
# Large test packages start first; stable tie-breaking is identical on every runner.
31+
for name, weight in sorted(packages, key=lambda item: (-item[1], item[0])):
32+
index = min(range(count), key=lambda i: (weights[i], i))
33+
shards[index].append(name)
34+
weights[index] += weight
35+
return [sorted(shard) for shard in shards]
36+
37+
38+
def merge(root, expected, output):
39+
count = len(expected)
40+
directories = {path.name for path in root.iterdir()}
41+
if directories != {f"backend-test-{i}" for i in range(1, count + 1)}:
42+
raise ValueError("missing or unexpected backend shard artifacts")
43+
blocks = {}
44+
events = []
45+
for index, packages in enumerate(expected, 1):
46+
directory = root / f"backend-test-{index}"
47+
manifest = json.loads((directory / "manifest.json").read_text(encoding="utf-8"))
48+
if manifest != {"index": index, "count": count, "packages": packages}:
49+
raise ValueError(f"shard {index}: package manifest differs from discovery")
50+
terminal = set()
51+
for line in (directory / "test-events.json").read_text(encoding="utf-8").splitlines():
52+
event = json.loads(line)
53+
package = event.get("Package")
54+
if package and package not in packages:
55+
raise ValueError(f"shard {index}: unexpected test package {package}")
56+
if event.get("Action") == "fail":
57+
raise ValueError(f"shard {index}: failing test event")
58+
if not event.get("Test") and event.get("Action") in {"pass", "skip"}:
59+
terminal.add(package)
60+
events.append(line)
61+
if terminal != set(packages):
62+
raise ValueError(f"shard {index}: incomplete package test events")
63+
profile = (directory / "coverage.out").read_text(encoding="utf-8").splitlines()
64+
if not profile or profile[0] != "mode: atomic":
65+
raise ValueError(f"shard {index}: expected atomic coverage profile")
66+
for line in profile[1:]:
67+
match = re.fullmatch(r"(.+:\d+\.\d+,\d+\.\d+) (\d+) (\d+)", line)
68+
if not match:
69+
raise ValueError(f"shard {index}: malformed coverage block")
70+
key, statements, hits = match.groups()
71+
statements, hits = int(statements), int(hits)
72+
if key in blocks and blocks[key][0] != statements:
73+
raise ValueError(f"shard {index}: inconsistent coverage block {key}")
74+
blocks[key] = (statements, hits + blocks.get(key, (0, 0))[1])
75+
if not blocks:
76+
raise ValueError("merged coverage is empty")
77+
output.mkdir(parents=True, exist_ok=True)
78+
(output / "coverage.out").write_text(
79+
"mode: atomic\n"
80+
+ "".join(f"{key} {statements} {hits}\n" for key, (statements, hits) in sorted(blocks.items())),
81+
encoding="utf-8",
82+
)
83+
(output / "test-events.json").write_text("\n".join(events) + "\n", encoding="utf-8")
84+
print(f"Merged {count} shards: {sum(map(len, expected))} packages, {len(blocks)} coverage blocks")
85+
86+
87+
def main():
88+
parser = argparse.ArgumentParser(description=__doc__)
89+
parser.add_argument("command", choices=["plan", "merge"])
90+
parser.add_argument("--count", type=int, required=True)
91+
parser.add_argument("--index", type=int)
92+
parser.add_argument("--input", type=Path)
93+
parser.add_argument("--output", type=Path, required=True)
94+
args = parser.parse_args()
95+
shards = partition(discover(), args.count)
96+
if args.command == "merge":
97+
if args.input is None:
98+
parser.error("merge requires --input")
99+
merge(args.input, shards, args.output)
100+
return
101+
if args.index is None or not 1 <= args.index <= args.count:
102+
parser.error("plan requires --index in 1..count")
103+
packages = shards[args.index - 1]
104+
args.output.mkdir(parents=True, exist_ok=True)
105+
(args.output / "packages.txt").write_text("\n".join(packages) + "\n", encoding="utf-8")
106+
(args.output / "manifest.json").write_text(json.dumps({
107+
"index": args.index, "count": args.count, "packages": packages,
108+
}), encoding="utf-8")
109+
print(f"Shard {args.index}/{args.count}: {len(packages)} packages")
110+
111+
112+
if __name__ == "__main__":
113+
main()
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import assert from 'node:assert/strict';
2+
import { readFileSync } from 'node:fs';
3+
import { createRequire } from 'node:module';
4+
import { test } from 'node:test';
5+
6+
const require = createRequire(new URL('../../web/package.json', import.meta.url));
7+
const { load } = require('js-yaml');
8+
const workflow = name => load(readFileSync(new URL(`../workflows/${name}`, import.meta.url), 'utf8'));
9+
const ci = workflow('ci.yml');
10+
const browser = workflow('frontend-quality.yml');
11+
const commands = job => job.steps.map(step => step.run ?? '').join('\n');
12+
13+
test('unit shard matrices match the strict merge counts and never fail-fast', () => {
14+
for (const domain of ['backend', 'frontend']) {
15+
const count = Number(ci.env[`${domain.toUpperCase()}_SHARDS`]);
16+
const job = ci.jobs[`${domain}-tests`];
17+
assert.deepEqual(job.strategy.matrix.shard, Array.from({ length: count }, (_, i) => i + 1));
18+
assert.equal(job.strategy['fail-fast'], false);
19+
assert.equal(job.needs, undefined);
20+
assert.equal(ci.jobs[`${domain}-coverage`].needs, `${domain}-tests`);
21+
assert.match(commands(ci.jobs[`${domain}-coverage`]), /ci_test_shards\.py merge|check_vitest_blobs\.mjs/);
22+
}
23+
assert.ok(ci.jobs['backend-tests'].services.postgres);
24+
assert.ok(ci.jobs['backend-database'].services.postgres);
25+
assert.match(commands(ci.jobs['backend-tests']), /go test -race .*covermode=atomic/);
26+
assert.match(commands(ci.jobs['frontend-tests']), /--maxWorkers=2/);
27+
});
28+
29+
test('existing aggregate names reject any required job that did not succeed', () => {
30+
for (const [jobs, id, name, dependencies] of [
31+
[ci.jobs, 'backend', 'Backend (lint + test + build)',
32+
['backend-checks', 'backend-tests', 'backend-coverage', 'backend-database', 'backend-build']],
33+
[ci.jobs, 'frontend', 'Frontend (lint + test + build)',
34+
['frontend-checks', 'frontend-tests', 'frontend-coverage']],
35+
[browser.jobs, 'chromium-quality', 'Chromium responsive, a11y, keyboard, and performance',
36+
['contract', 'browser-build', 'chromium-tests']],
37+
[browser.jobs, 'visual', 'Deliberate visual snapshot gate (Windows baseline)',
38+
['contract', 'browser-build', 'visual-tests']],
39+
]) {
40+
const job = jobs[id];
41+
assert.equal(job.name, name);
42+
assert.equal(job.if, 'always()');
43+
assert.deepEqual(job.needs, dependencies);
44+
assert.match(commands(job), /all\(job\["result"\] == "success" for job in results\.values\(\)\)/);
45+
assert.equal(job.steps[0].env.RESULTS, '${{ toJSON(needs) }}');
46+
}
47+
});
48+
49+
test('independent validation runs in parallel but Docker waits for successful gates', () => {
50+
for (const id of ['generated', 'backend-checks', 'backend-build', 'backend-database', 'frontend-checks']) {
51+
assert.equal(ci.jobs[id].needs, undefined);
52+
}
53+
assert.deepEqual(ci.jobs.docker.needs, ['generated', 'backend', 'frontend']);
54+
assert.equal(ci.jobs.docker.if, undefined);
55+
const build = ci.jobs.docker.steps.find(step => step.uses?.startsWith('docker/build-push-action'));
56+
assert.equal(build.with.push, false);
57+
});
58+
59+
test('browser shards reuse one build and retain isolated performance and Windows baselines', () => {
60+
const shards = browser.jobs['chromium-tests'].strategy.matrix.include;
61+
assert.deepEqual(shards.filter(shard => shard.suite === 'quality').map(shard => shard.shard),
62+
['1/4', '2/4', '3/4', '4/4']);
63+
for (const suite of ['a11y', 'performance']) {
64+
assert.deepEqual(shards.filter(shard => shard.suite === suite).map(shard => shard.shard), ['1/1']);
65+
}
66+
for (const id of ['chromium-tests', 'visual-tests', 'cross-browser']) {
67+
const job = browser.jobs[id];
68+
assert.equal(job.needs, 'browser-build');
69+
assert.equal(job.strategy['fail-fast'], false);
70+
assert.ok(job.steps.some(step => step.with?.name === 'e2e-app' && step.uses?.startsWith('actions/download-artifact')));
71+
assert.equal(job.steps.find(step => step.env?.E2E_REUSE_BUILD)?.env.E2E_REUSE_BUILD, '1');
72+
}
73+
assert.equal(browser.jobs['visual-tests']['runs-on'], 'windows-latest');
74+
assert.deepEqual(browser.jobs['visual-tests'].strategy.matrix.shard, [1, 2, 3, 4]);
75+
assert.equal(Object.values(browser.jobs).filter(job => commands(job).includes('npm run e2e:build')).length, 1);
76+
});
77+
78+
test('parallelization introduces no additional nonblocking test exceptions', () => {
79+
const waivers = Object.entries(ci.jobs).flatMap(([job, config]) =>
80+
config.steps.filter(step => step['continue-on-error']).map(step => [job, step.name]));
81+
assert.deepEqual(waivers, [['backend-database', 'Integration test (telemetry replay)']]);
82+
});

0 commit comments

Comments
 (0)