Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -615,7 +615,15 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 10
needs: [ci-success]
if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request' && needs.ci-success.result == 'success'
# QNBS-v3 (#522): always()+!cancelled() overrides GHA's default skip-propagation from pr-size/rust-tauri/core-rust being legitimately skipped on push, without publishing from a cancelled run — ci-success.result is the real gate.
if: >-
${{
always() &&
!cancelled() &&
github.ref == 'refs/heads/main' &&
github.event_name != 'pull_request' &&
needs.ci-success.result == 'success'
}}
permissions:
contents: read
pages: write
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
<img src="https://img.shields.io/badge/Storage-IndexedDB_v8-F59E0B" alt="IndexedDB v8">
<img src="https://img.shields.io/badge/PWA-v3.0-5BB974?logo=pwa" alt="PWA v3.0">
<img src="https://img.shields.io/badge/i18n-19_locales-2925_keys-0EA5E9" alt="i18n 19 locales — 2925 keys">
<img src="https://img.shields.io/badge/Tests-7167%2B_%2F_588_files-22C55E" alt="7167+ tests / 588 files">
<img src="https://img.shields.io/badge/Tests-7168%2B_%2F_588_files-22C55E" alt="7168+ tests / 588 files">
<img src="https://img.shields.io/codecov/c/github/qnbs/WorldScript-Studio?logo=codecov&label=Coverage" alt="Codecov Coverage">
<img src="https://img.shields.io/badge/License-MIT-22C55E" alt="License MIT">
<img src="https://img.shields.io/github/actions/workflow/status/qnbs/WorldScript-Studio/.github/workflows/ci.yml?branch=main&logo=github" alt="CI Status">
Expand Down Expand Up @@ -512,7 +512,7 @@ The Settings → AI panel shows a live GPU status badge with adapter details and
| **Document Export** | docx + jszip | Word-compatible `.docx` generation (lazy-loaded) |
| **PWA** | Service Worker + Web App Manifest v3 | Offline support, installability, Workbox chunking |
| **i18n** | Custom React Context (`I18nContext.tsx`) | 2925 keys × 19 locales (de/en/es/fr/it + ar/he/fa RTL Beta + ja/zh/pt/el/fi/sv/hu/is/eu/ru/ko Beta); EN fallback; `localStorage` persistence |
| **Testing** | Vitest 4.x (7167+ tests / 588 files) + Playwright E2E | Unit/integration + cross-browser E2E; Stryker mutation (manual workflow) |
| **Testing** | Vitest 4.x (7168+ tests / 588 files) + Playwright E2E | Unit/integration + cross-browser E2E; Stryker mutation (manual workflow) |
| **Code Quality** | Biome (lint + format) + TypeScript 7 (tsgo) strict | `--error-on-warnings` in CI; zero `any` policy |
| **Visualization** | Force-directed graph | Interactive character relationship network |
| **Desktop** | Tauri v2 | Cross-platform installer; auto-updater via `latest.json` |
Expand Down Expand Up @@ -550,7 +550,7 @@ WorldScript-Studio/
│ ├── sw.js # PWA Service Worker
│ └── manifest.json # PWA Web App Manifest v3
├── tests/
│ ├── unit/ # Vitest unit tests (7167+ tests, 588 files) — count spans tests/, components/, packages/*/tests/, not just this folder
│ ├── unit/ # Vitest unit tests (7168+ tests, 588 files) — count spans tests/, components/, packages/*/tests/, not just this folder
│ │ ├── ai/ # aiSmallModules, aiCoreFallbackPaths
│ │ └── settings/ # WebLlmPanel, AiSections
│ └── e2e/ # Playwright specs + helpers.ts
Expand Down Expand Up @@ -712,7 +712,7 @@ The main pipeline is [`.github/workflows/ci.yml`](.github/workflows/ci.yml). Opt
| `scorecard` | weekly + `main` push | OpenSSF Scorecard — SARIF uploaded to GitHub Code Scanning |

**Current test metrics (2026-08-21, source-synchronized; CI remains authoritative for pass/fail):**
- **7167+ unit tests** across **588 test files** — CI is authoritative for pass/fail
- **7168+ unit tests** across **588 test files** — CI is authoritative for pass/fail
- Coverage thresholds: lines ≥ 80 · branches ≥ 66 · functions ≥ 72 · statements ≥ 78 — enforced in CI (see Codecov badge for live metrics)
- i18n: **2925 keys × 19 locales** (en/de/fr/es/it + ar/he/fa RTL Beta + ja/zh/pt/el/fi/sv/hu/is/eu/ru/ko Beta)

Expand Down
17 changes: 17 additions & 0 deletions tests/unit/workflowPolicy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { fileURLToPath } from 'node:url';
import { describe, expect, it } from 'vitest';
import {
extractJobBlock,
extractJobIf,
extractJobNames,
extractLocalPathDependencies,
extractNeeds,
Expand Down Expand Up @@ -127,6 +128,22 @@ describe('CI workflow policy', () => {
expect(visited).toContain('ci-success');
});

// QNBS-v3 (#522): deploy's default success() gate inherits skip-propagation from pr-size/rust-tauri/core-rust being legitimately skipped on push, even though ci-success itself computed 'success' — always() overrides that and makes ci-success.result the real gate.
it('gates deploy on ci-success.result explicitly via always()+!cancelled(), not implicit success()', () => {
const deployBlock = extractJobBlock(workflowSource, 'deploy');
const deployIf = extractJobIf(deployBlock);
expect(extractNeeds(workflowSource, 'deploy')).toEqual(['ci-success']);
// QNBS-v3 (#522): main push has pr-size/rust-tauri/core-rust legitimately skipped and ci-success == success — deploy must still be eligible, not silently skipped by GHA's default chain propagation.
expect(deployIf).toContain('always()');
expect(deployIf).toContain('!cancelled()');
expect(deployIf).toMatch(/github\.ref == 'refs\/heads\/main'/);
expect(deployIf).toMatch(/github\.event_name != 'pull_request'/);
expect(deployIf).toMatch(/needs\.ci-success\.result == 'success'/);
expect(deployIf).toMatch(
/always\(\)[\s\S]+!cancelled\(\)[\s\S]+github\.ref == 'refs\/heads\/main'[\s\S]+github\.event_name != 'pull_request'[\s\S]+needs\.ci-success\.result == 'success'/,
);
Comment thread
qnbs marked this conversation as resolved.
});
Comment thread
qnbs marked this conversation as resolved.

// QNBS-v3: Keep every unconditional CI job explicitly required or advisory so deploy cannot false-green.
it('keeps required and advisory job authority explicit', () => {
const ciSuccessBlock = extractJobBlock(workflowSource, 'ci-success');
Expand Down
11 changes: 11 additions & 0 deletions tests/utils/workflowPolicyParsers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,17 @@ export function extractJobBlock(workflowSource: string, jobName: string): string
return lines.slice(start, end === -1 ? lines.length : end).join('\n');
}

// QNBS-v3 (#522): scoped to the job's own if: value, not the whole block — a QNBS-v3 comment on the preceding line can otherwise contain the same tokens an assertion is checking for, letting a regressed if: expression still pass.
export function extractJobIf(jobBlock: string): string {
const lines = jobBlock.split('\n');
const start = lines.findIndex((line) => /^ {4}if: ?/.test(line));
if (start === -1) throw new Error('Could not find an if: field in this job block');
const firstLine = lines[start] as string;
if (!/^ {4}if: >-?\s*$/.test(firstLine)) return firstLine.replace(/^ {4}if: /, '');
const end = lines.findIndex((line, index) => index > start && !/^ {5,}/.test(line));
return lines.slice(start + 1, end === -1 ? lines.length : end).join('\n');
}

// QNBS-v3: Named step extraction keeps policy assertions scoped to the active workflow step.
export function extractStepBlock(jobBlock: string, stepName: string): string {
const lines = jobBlock.split('\n');
Expand Down
Loading