Compare commits

...
Author SHA1 Message Date
james.maes 3396e6da8c Merge remote-tracking branch 'origin/main' into fix/windows-source-headers-20261101
Signed-off-by: James Maes <james@kof22.com>

# Conflicts:
#	docs/SESSION-STATE.md
2026-11-01 12:12:28 -06:00
james.maesandCodex GPT-6 feea631d17 feat: show Query and QRun links in About
Bundle the official mascot and keep customer identity, credits and native navigation accessible with long names in both themes.

Co-Authored-By: Codex GPT-6 <noreply@openai.com>
Signed-off-by: James Maes <james@kof22.com>
2026-11-01 12:11:58 -06:00
james.maesandCodex GPT-6 e1961e6bd0 chore: enforce owned source copyright notices
Check real leading comments against explicit repository provenance and retain existing native style gates.

Co-Authored-By: Codex GPT-6 <noreply@openai.com>
Signed-off-by: James Maes <james@kof22.com>
2026-11-01 12:11:01 -06:00
james.maesandCodex e5846df715 docs: record verified release and in-app update acceptance (#13)
* docs: record verified 0.3.2 prerelease

Record the published 0.3.2 release and independently verified asset/MSI evidence, replacing stale upcoming-release wording. Preserve installed 0.3.1 evidence and explicitly retain pending in-app update and installed-provider acceptance. All three Markdown files and diff checks pass; no runtime changes. Prepared and reviewed with Codex (GPT-6).

Co-Authored-By: Codex (GPT-6) <noreply@openai.com>
Signed-off-by: James Maes <james@kof22.com>

* docs: record verified installed in-app update acceptance

Record the real0.3.1-to-0.3.2 Not now and Update and restart flow, exact published bytes, old-helper handoff, MSI success, retained history and restored session/WebSocket with operator-confirmed automatic reopen. Preserve the original diagnostic summary-writer failure separately from independently validated successful child reports.

Keep physical evidence source-scoped and retain installed-provider, reboot/failure-path and original layout-cause limits. Four Markdown files and diff checks pass; no runtime changes.

Co-Authored-By: Codex (GPT-6) <noreply@openai.com>
Signed-off-by: James Maes <james@kof22.com>

---------

Signed-off-by: James Maes <james@kof22.com>
Co-authored-by: Codex (GPT-6) <noreply@openai.com>
2026-11-01 11:14:31 -06:00
20 changed files with 951 additions and 26 deletions

No files matched your search

+128
View File
@@ -0,0 +1,128 @@
# Copyright (c) 2026 James Maes
# SPDX-License-Identifier: MIT
"""Check leading notices in explicitly owned, Git-tracked source families."""
import fnmatch
import json
import re
import subprocess
import sys
from pathlib import Path
from typing import Any
def selected(name: str, policy: dict[str, Any]) -> bool:
"""Select owned paths, honoring documented exact/pattern exclusions first."""
if any(fnmatch.fnmatchcase(name, item["pattern"]) for item in policy["exclusions"]):
return False
return any(fnmatch.fnmatchcase(name, pattern) for pattern in policy["includes"])
def leading_comments(text: str, style: str) -> str:
"""Read comments only, preserving required interpreter/XML directive ordering."""
text = text.removeprefix("\ufeff")
if text.startswith("<?xml"):
declaration = re.match(r"<\?xml[^?]*\?>", text)
if style != "xml" or declaration is None:
raise ValueError("Invalid XML declaration")
text = text[declaration.end():]
elif text.startswith("#!"):
if style not in ("hash", "powershell", "slash"):
raise ValueError("Unexpected interpreter directive")
text = text.partition("\n")[2]
while re.match(r"#Requires\b", text, re.IGNORECASE):
if style != "powershell":
raise ValueError("Unexpected PowerShell directive")
text = text.partition("\n")[2]
comments = []
while text.strip():
text = text.lstrip()
if text.startswith("<?xml") or re.match(r"#!|#Requires\b", text, re.IGNORECASE):
raise ValueError("Required declaration must precede the notice")
if style in ("hash", "powershell") and text.startswith("#"):
line, _, text = text.partition("\n")
comments.append(line[1:])
elif style == "slash" and text.startswith("//"):
line, _, text = text.partition("\n")
comments.append(line[2:])
elif ((style == "slash" and text.startswith("/*"))
or (style == "xml" and text.startswith("<!--"))
or (style == "powershell" and text.startswith("<#"))):
opening, closing = {"slash": ("/*", "*/"), "xml": ("<!--", "-->"), "powershell": ("<#", "#>")}[style]
body, separator, rest = text[len(opening):].partition(closing)
if not separator:
raise ValueError("Unterminated leading comment")
comments.append(body)
text = rest
else:
break
return "\n".join(re.sub(r"^\s*[*~]\s?", "", line).strip() for line in "\n".join(comments).splitlines())
def check_notice(name: str, text: str, policy: dict[str, Any]) -> None:
"""Require an approved holder/license pair in real leading comment syntax."""
suffix = Path(name).suffix
style = policy["styles"].get(suffix)
if Path(name).name.startswith("Dockerfile."):
style = "hash"
if style is None:
raise ValueError("Selected path has no configured comment syntax")
comments = leading_comments(text, style)
approved = [item for item in policy["notices"] if any(fnmatch.fnmatchcase(name, pattern) for pattern in item["paths"])]
for notice in approved:
holder = re.escape(notice["holder"])
copyright_pattern = rf"(?m)^Copyright\s+(?:\([cC]\)\s*)?\d{{4}}(?:[-–]\d{{4}})?\.?\s+{holder}(?:\s+\(KofTwentyTwo\))?\.?(?:\s+Licensed under the MIT License \(see LICENSE\)\.)?$"
if re.search(copyright_pattern, comments) is None:
continue
spdx = re.findall(r"(?m)^SPDX-License-Identifier:\s*(\S+)\s*$", comments)
if spdx and any(value != notice["license"] for value in spdx):
continue
apache = all(phrase in comments for phrase in (
"Licensed under the Apache License, Version 2.0",
"you may not use this file except in compliance with the License.",
"https://www.apache.org/licenses/LICENSE-2.0",
"WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND",
"limitations under the License.",
))
if suffix == ".java" and not apache:
continue
if notice["license"] in spdx or (notice["license"] == "Apache-2.0" and apache):
return
raise ValueError("Missing or mismatched leading copyright/license notice")
def check_repository(root: Path, policy: dict[str, Any]) -> int:
"""Check tracked sources without scanning build outputs or executing source."""
tracked = subprocess.run(["git", "-C", str(root), "ls-files", "-z"], check=True, capture_output=True).stdout.decode("utf-8").split("\u0000")
failures = []
count = 0
for name in tracked:
if not name or not selected(name, policy):
continue
count += 1
try:
check_notice(name, (root / name).read_text(encoding="utf-8-sig"), policy)
except (ValueError, OSError) as error:
failures.append(f"{name}: {error}")
if count == 0:
raise ValueError("No owned source files selected")
if failures:
raise ValueError("\n".join(failures))
return count
def main() -> None:
"""Load the repository-specific reviewed scope and fail CI on a bad notice."""
root = Path(__file__).resolve().parents[2]
policy = json.loads((root / ".github/source-headers.json").read_text(encoding="utf-8"))
count = check_repository(root, policy)
print(f"Leading source notices verified: {count} tracked files")
if __name__ == "__main__":
try:
main()
except (ValueError, OSError, subprocess.CalledProcessError) as error:
print(str(error), file=sys.stderr)
sys.exit(1)
+156
View File
@@ -0,0 +1,156 @@
# Copyright (c) 2026 James Maes
# SPDX-License-Identifier: MIT
"""Exercise notice parsing, provenance, scope and real Git inventory rejection."""
import importlib.util
import json
import subprocess
import tempfile
import unittest
from pathlib import Path
from typing import Any
SPEC = importlib.util.spec_from_file_location("headers", Path(__file__).with_name("check-source-headers.py"))
assert SPEC is not None and SPEC.loader is not None
HEADERS = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(HEADERS)
POLICY = json.loads(Path(__file__).parents[1].joinpath("source-headers.json").read_text(encoding="utf-8"))
MIT = "Copyright (c) 2026 James Maes\nSPDX-License-Identifier: MIT"
APACHE = """Copyright (C) 2021-2025. Kingsrook, LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License."""
def fixture_policy(holder: str = "James Maes", license_id: str = "MIT") -> dict[str, Any]:
"""Use the production styles with a small explicit owned fixture scope."""
return {"includes": ["src/*"], "exclusions": [], "styles": POLICY["styles"],
"notices": [{"holder": holder, "license": license_id, "paths": ["src/*"]}]}
class HeaderTests(unittest.TestCase):
"""Ensure policy failures cannot pass through text-only copyright searches."""
def test_each_real_comment_syntax(self) -> None:
"""Recognize hash, slash, block and XML notices in their own languages."""
for name, text in (
("src/a.py", "# " + MIT.replace("\n", "\n# ") + "\nprint(1)"),
("src/a.cs", "// " + MIT.replace("\n", "\n// ") + "\nclass A {}"),
("src/a.mjs", "/*\n * " + MIT.replace("\n", "\n * ") + "\n */\nexport {};"),
("src/a.xml", "<!--\n" + MIT + "\n-->\n<root/>"),
("src/a.ps1", "<#\n" + MIT + "\n#>\nWrite-Output 1"),
):
with self.subTest(name=name):
HEADERS.check_notice(name, text, fixture_policy())
def test_required_first_lines_and_bom(self) -> None:
"""Accept shebangs, PowerShell requirements and XML declarations first."""
for name, prefix, body in (
("src/a.sh", "#!/bin/sh\n", "# " + MIT.replace("\n", "\n# ")),
("src/a.ps1", "#Requires -Version 7.4\n", "# " + MIT.replace("\n", "\n# ")),
("src/a.xml", "<?xml version=\"1.0\"?>\n", "<!--" + MIT + "--><root/>"),
("src/a.mjs", "#!/usr/bin/env node\n", "/*" + MIT + "*/"),
):
with self.subTest(name=name):
HEADERS.check_notice(name, "\ufeff" + prefix + body, fixture_policy())
def test_misordered_required_first_lines_fail(self) -> None:
"""A notice must not displace an interpreter or XML declaration."""
for name, text in (
("src/a.sh", "# " + MIT.replace("\n", "\n# ") + "\n#!/bin/sh"),
("src/a.ps1", "# " + MIT.replace("\n", "\n# ") + "\n#Requires -Version 7.4"),
("src/a.xml", "<!--" + MIT + "--><?xml version=\"1.0\"?><root/>"),
):
with self.subTest(name=name), self.assertRaises(ValueError):
HEADERS.check_notice(name, text, fixture_policy())
def test_code_strings_and_late_comments_do_not_count(self) -> None:
"""Reject docstrings and comments after executable declarations."""
for text in ("notice = \"" + MIT.replace("\n", "\\n") + "\"",
"\"\"\"" + MIT + "\"\"\"", "import os\n# " + MIT.replace("\n", "\n# ")):
with self.subTest(text=text), self.assertRaises(ValueError):
HEADERS.check_notice("src/a.py", text, fixture_policy())
def test_wrong_syntax_and_unterminated_comments_fail(self) -> None:
"""Notice text alone is insufficient without a complete language comment."""
for text in ("# " + MIT, "/*" + MIT, "string text = \"" + MIT + "\";"):
with self.subTest(text=text), self.assertRaises(ValueError):
HEADERS.check_notice("src/a.cs", text, fixture_policy())
with self.assertRaises(ValueError):
HEADERS.check_notice("src/a.py", "<#" + MIT + "#>", fixture_policy())
def test_wrong_holder_or_license_and_conflicting_spdx_fail(self) -> None:
"""Do not conflate repository licenses or accept conflicting identifiers."""
for text in (MIT.replace("James Maes", "Someone Else"), MIT.replace("MIT", "Apache-2.0"),
MIT + "\nSPDX-License-Identifier: Apache-2.0", MIT.splitlines()[0]):
with self.subTest(text=text), self.assertRaises(ValueError):
HEADERS.check_notice("src/a.cs", "/*" + text + "*/", fixture_policy())
def test_complete_legacy_apache_without_spdx(self) -> None:
"""Retain valid legacy year ranges and complete Apache notices."""
policy = fixture_policy("Kingsrook, LLC", "Apache-2.0")
HEADERS.check_notice("src/A.java", "/*" + APACHE + "*/\nclass A {}", policy)
HEADERS.check_notice("src/a.xml", "<!--" + APACHE + "--><root/>", policy)
def test_java_requires_full_apache_block(self) -> None:
"""An abbreviated SPDX-only Java notice cannot replace scoped convention."""
policy = fixture_policy("Kingsrook, LLC", "Apache-2.0")
for notice in ("Copyright 2026 Kingsrook, LLC\nSPDX-License-Identifier: Apache-2.0",
APACHE.replace("limitations under the License.", "")):
with self.subTest(notice=notice), self.assertRaises(ValueError):
HEADERS.check_notice("src/A.java", "/*" + notice + "*/", policy)
def test_virtual_agent_provenance_is_path_scoped(self) -> None:
"""Only the owned virtual-agent family may use its existing QRun.IO holder."""
policy = fixture_policy()
policy["notices"].append({"holder": "QRun.IO, Inc.", "license": "MIT", "paths": ["tools/virtual-agent/*"]})
text = "/*Copyright 2026 QRun.IO, Inc. Licensed under the MIT License (see LICENSE).\nSPDX-License-Identifier: MIT*/"
HEADERS.check_notice("tools/virtual-agent/a.mjs", text, policy)
with self.assertRaises(ValueError):
HEADERS.check_notice("src/a.mjs", text, policy)
def test_current_scope_and_documented_exclusions(self) -> None:
"""New scripts enter the guard; data, generated and upstream files do not."""
self.assertTrue(HEADERS.selected(".github/scripts/new-tool.py", POLICY))
self.assertTrue(HEADERS.selected(".github/workflows/new-check.yml", POLICY))
for name in (".mvn/wrapper/MavenWrapperDownloader.java", "mvnw", "schemas/new.json",
"src/main/resources/openapi/new.yaml", "src/test/resources/openapi/old.yaml",
"src/generated/Example.cs", "src/Example/obj/Generated.cs", "packages.lock.json"):
with self.subTest(name=name):
self.assertFalse(HEADERS.selected(name, POLICY))
self.assertTrue(all(item["reason"].strip() for item in POLICY["exclusions"]))
def test_guard_sources_match_this_repository_provenance(self) -> None:
"""Check copied guard files against local policy even before Git staging."""
for filename in ("check-source-headers.py", "test-source-headers.py"):
path = Path(__file__).with_name(filename)
name = ".github/scripts/" + filename
self.assertTrue(HEADERS.selected(name, POLICY))
HEADERS.check_notice(name, path.read_text(encoding="utf-8"), POLICY)
def test_real_git_inventory_rejects_missing_tracked_header(self) -> None:
"""A tracked new source fails while untracked generated output is ignored."""
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
subprocess.run(["git", "init", "-q", str(root)], check=True)
root.joinpath("src").mkdir()
source = root / "src/a.py"
source.write_text("print(1)", encoding="utf-8")
subprocess.run(["git", "-C", str(root), "add", "--", "src/a.py"], check=True)
with self.assertRaisesRegex(ValueError, "src/a.py"):
HEADERS.check_repository(root, fixture_policy())
source.write_text("# " + MIT.replace("\n", "\n# ") + "\nprint(1)", encoding="utf-8")
root.joinpath("src/untracked.py").write_text("print(2)", encoding="utf-8")
self.assertEqual(1, HEADERS.check_repository(root, fixture_policy()))
if __name__ == "__main__":
unittest.main()
+126
View File
@@ -0,0 +1,126 @@
{
"description": "Scope of the reviewed 2026-11-01 owned-source notice pass. Python fnmatch patterns match slash-containing relative paths. Native C# IDE0073 and full Java Apache conventions remain in force.",
"includes": [
".github/*.yml",
".github/*.yaml",
".github/scripts/*.py",
".pre-commit-config.yaml",
"Directory.Build.props",
"Directory.Packages.props",
"PSScriptAnalyzerSettings.psd1",
"build/*.ps1",
"installer/*.props",
"installer/*.wxs",
"installer/*.wixproj",
"src/*.cs",
"src/*.xaml",
"src/*.csproj",
"tests/*.cs",
"tests/*.csproj",
"tests/*.ps1",
"tools/virtual-agent/*.mjs"
],
"exclusions": [
{
"pattern": "*.json",
"reason": "Schema, fixtures, manifests and lock data: repository LICENSE/NOTICE, no comments."
},
{
"pattern": "*.lock",
"reason": "Dependency lock data, not owned executable source."
},
{
"pattern": "*/generated/*",
"reason": "Explicit generated-code directory."
},
{
"pattern": "*/bin/*",
"reason": "Generated build output."
},
{
"pattern": "*/obj/*",
"reason": "Generated build output."
},
{
"pattern": "*/node_modules/*",
"reason": "Third-party dependency output."
},
{
"pattern": ".mvn/*",
"reason": "Upstream Maven wrapper provenance."
},
{
"pattern": "mvnw*",
"reason": "Upstream Maven wrapper provenance."
},
{
"pattern": "Assets/Legal/*",
"reason": "Byte-exact third-party legal files."
},
{
"pattern": "src/main/resources/*",
"reason": "Bundled font/legal assets and OpenAPI schema data."
},
{
"pattern": "src/test/resources/openapi/*",
"reason": "Historical contract schema fixtures."
},
{
"pattern": "*.slnx",
"reason": "Solution metadata outside the completed source-header pass; root LICENSE/NOTICE applies."
},
{
"pattern": "*.manifest",
"reason": "Application activation metadata outside this pass; root LICENSE/NOTICE applies."
},
{
"pattern": "*.runsettings",
"reason": "Test runner metadata outside this pass; root LICENSE/NOTICE applies."
},
{
"pattern": ".editorconfig",
"reason": "Style configuration outside this pass; root LICENSE/NOTICE applies."
}
],
"styles": {
".py": "hash",
".ps1": "powershell",
".psd1": "powershell",
".sh": "hash",
".yml": "hash",
".yaml": "hash",
".cs": "slash",
".java": "slash",
".mjs": "slash",
".js": "slash",
".xml": "xml",
".props": "xml",
".csproj": "xml",
".wxs": "xml",
".wixproj": "xml",
".xaml": "xml"
},
"notices": [
{
"paths": [
".github/*",
".pre-commit-config.yaml",
"Directory.*.props",
"PSScriptAnalyzerSettings.psd1",
"build/*",
"installer/*",
"src/*",
"tests/*"
],
"license": "MIT",
"holder": "James Maes"
},
{
"paths": [
"tools/virtual-agent/*"
],
"license": "MIT",
"holder": "QRun.IO, Inc."
}
]
}
+4
View File
@@ -29,6 +29,10 @@ jobs:
with:
node-version: '24'
package-manager-cache: false
- name: Verify owned source notices and rejection paths
run: |
python3 .github/scripts/test-source-headers.py
python3 .github/scripts/check-source-headers.py
- name: Verify virtual-agent protocol and HTTP boundaries
run: node --test tools/virtual-agent/test/virtual-agent.test.mjs
+4
View File
@@ -26,3 +26,7 @@ redistribution or end-user protection obligation.
The QQQ framework, qbit-printing and qqq-print-protocol are separate repositories;
this notice does not change their licensing.
The About dialog bundles Query, the QQQ and QRun.IO dog mascot, supplied by
QRun.IO. The unmodified artwork and provenance are in Assets/Query in the
application payload and src/Qqq.PrintAgent/Assets/Query in this repository.
+21 -4
View File
@@ -11,9 +11,9 @@ Print from **QQQ 4.1+** to printers at a local facility without opening inbound
The native setup and status application uses the same [KofTwentyTwo AppKit](https://github.com/KofTwentyTwo/AppKit) foundation as GCLO and MarkdownViewr.Windows.
> **Published prerelease: [0.3.1-rc.1](https://github.com/QRun-IO/qqq-print-agent/releases/tag/v0.3.1-rc.1).** Its published assets and attestations were verified, and the exact MSI was installed with pairing, printer profiles and local history preserved. The operator confirmed successful test prints with a responsive window. The installer remains unsigned. See [acceptance notes](docs/ACCEPTANCE.md) for the scope of that evidence.
> **Published and installed prerelease: [0.3.2-rc.1](https://github.com/QRun-IO/qqq-print-agent/releases/tag/v0.3.2-rc.1).** All four assets, checksums and provenance attestations were verified. The real in-app update completed with matching app/service/CLI builds, retained identity and history, and an automatically reopened Connected app. The installer remains unsigned.
The upcoming **0.3.2-rc.1** checks the app, service and CLI for the same complete
**0.3.2-rc.1** checks the app, service and CLI for the same complete
build version after an update, reports an inconsistent installation explicitly,
and corrects the SDK license credit. Its native test host now uses production's
PerMonitorV2 DPI declaration. The original 0.3.0 layout-cycle cause remains
@@ -21,7 +21,14 @@ unproven; current operator acceptance and repeated native replays pass. The
existing fatal-error handling is a mitigation, not an underlying layout fix.
See [changelog](CHANGELOG.md) and [session state](docs/SESSION-STATE.md).
The 0.3 client requires discovered printing extension `>=2.0.0 <3.0.0` and standard QQQ sign-in. Upgrade the server before adopting it; a 1.0 server cannot provide realtime printing. The service opens authenticated WSS for controls, receipts, status and job notifications, while documents stream separately over HTTPS. There is no job/control polling or fallback. Existing unbound pairings require explicit account association, not automatic replacement. See [realtime operation](docs/REALTIME.md) and [sign-in and migration](docs/QQQ-SIGN-IN.md). Configured Authentik/provider renewal and installed browser-callback acceptance remain separate qualification work.
The operator used **Help → Check for updates**, chose **Not now** and confirmed
the app remained responsive, then selected **Update and restart**. Independent
before/after reports verified the published 0.3.2 bytes and preserved earlier
jobs, pairing and profiles; the operator confirmed the reopened Connected app
and both printers. Successful paper output was previously confirmed on
**0.3.1-rc.1** and remains separate evidence; see [acceptance notes](docs/ACCEPTANCE.md).
The 0.3 client requires discovered printing extension `>=2.0.0 <3.0.0` and standard QQQ sign-in. Upgrade the server before adopting it; a 1.0 server cannot provide realtime printing. The service opens authenticated WSS for controls, receipts, status and job notifications, while documents stream separately over HTTPS. There is no job/control polling or fallback. Existing unbound pairings require explicit account association, not automatic replacement. See [realtime operation](docs/REALTIME.md) and [sign-in and migration](docs/QQQ-SIGN-IN.md). Real Authentik login and renewal passed in isolated development containers against an unreleased QQQ API 1.9 candidate. Released RC.20 lacks that native-session grant; installed browser/private-callback acceptance still requires a compatible published runtime and configured HTTPS provider.
## Current source features
@@ -87,7 +94,17 @@ A printer visible only in a signed-in user's profile may not be available to the
## Validation so far
The realtime contract and Jobs UI are included in the 0.3 release candidate. Focused application/UI-test builds passed with zero warnings/errors; three headless request-generation tests passed. They do not execute the desktop assertions or qualify installed realtime behavior. Full consumer, slow-transfer/control, reconnect/replay and installed acceptance are tracked separately in [session state](docs/SESSION-STATE.md).
The 0.3.2 Release gate passed **426 tests** with zero warnings/errors/skips.
[Release workflow 41324312106](https://github.com/QRun-IO/qqq-print-agent/actions/runs/41324312106)
completed all nine jobs and published the release for the signed tag at protected main
`268f951b6728b0e69fedaf46769785c3f5ae55f4`. All four release assets and their
attestations passed verification. Static MSI inspection confirmed product
version `0.3.2`, matching app/service/CLI source versions, WinUI resources,
service configuration and callback registration. The separately observed
0.3.1-to-0.3.2 in-app update returned MSI exit 0, restored the service's WebSocket
and automatically reopened the companion. Actual failed/canceled updates,
restart-required/reboot behavior and installed provider-browser acceptance
remain open. See [session state](docs/SESSION-STATE.md).
### Installed published 0.2.0 — 2026-10-31
+51 -7
View File
@@ -1,6 +1,52 @@
# Acceptance evidence
## Installed published 0.3.1 and next updater candidate — 2026-11-01
## Installed 0.3.2 through the real in-app update — 2026-11-01
The [0.3.2 release](https://github.com/QRun-IO/qqq-print-agent/releases/tag/v0.3.2-rc.1)
was verified against signed tag source
`268f951b6728b0e69fedaf46769785c3f5ae55f4`, all four assets/checksums and
GitHub attestations. [Release run 41324312106](https://github.com/QRun-IO/qqq-print-agent/actions/runs/41324312106)
completed all nine jobs. Published and installed MSI SHA-256:
`1191C337265469EB06136AF232DB57FAC6BC5014357521D80B35E5CCA5AF6E18`.
The MSI remains unsigned; source signatures and build attestations are separate
from publisher code signing.
The user operated the installed companion's **Help → Check for updates**, chose
**Not now** and confirmed continued responsiveness, then chose **Update and
restart**. The actual coordinator handed off to the copied installed 0.3.1 CLI
helper, MSI returned 0, and the companion automatically reopened. The user
confirmed its **Connected** state and both printers.
Independent before/after observations verified each App, Service and CLI
executable against the published payload bytes and identical full ProductVersion
`0.3.2-rc.1+268f951b6728b0e69fedaf46769785c3f5ae55f4`. The retained update request
matched the original companion identity, copied helper and published MSI. The
new service was Running with automatic LocalSystem configuration and restored
the native QQQ session and connected WebSocket. Stable identity, pairing,
printer exposure and profiles were preserved. All 38 prior local job rows and
37 prior server job rows retained their recorded field digests; one additional
row in each view was accounted for separately, with no missing or changed old
rows. The observers submitted no jobs and did not repeat the installation.
The diagnostic launcher's final summary failed after its observer child exited
successfully: reading stdout while its writer remained open caused a Windows
file-sharing violation. The original failed launcher record was preserved.
Both original child reports passed, and their hash chain, published-byte matches
and preservation comparisons were independently revalidated. This acceptance
uses those independent reports and the operator's UI confirmation; it does not
claim that the diagnostic launcher passed.
The old 0.3.1 helper executed this update. The new three-component guard is
covered by source regressions and protects future updates. Actual download or
installer failure/cancellation, restart-required reporting and reboot recovery
remain separate cases. Successful paper output was confirmed on 0.3.1 below;
this update check does not claim new 0.3.2 physical-output qualification. The
historical layout-cycle cause remains unproven. Real Authentik container checks
use an unreleased QQQ API 1.9 candidate and an HTTPS probe callback; installed
Windows browser/private-callback acceptance still requires a compatible
published QQQ runtime and configured HTTPS provider. No stable 1.0 claim is made.
## Installed published 0.3.1 — 2026-11-01
The [0.3.1 release](https://github.com/QRun-IO/qqq-print-agent/releases/tag/v0.3.1-rc.1)
was verified against its signed tag, source
@@ -26,12 +72,10 @@ outer window size. The original layout-cycle cause remains unproven. The exact
fatal-error mitigation was separately qualified with a real induced native
exception; this is not an underlying layout fix.
The next updater candidate rejects missing, old, unreadable and mixed full build
versions across app, service and CLI. It passed 73 focused updater cases and the
integrated 426-test Release gate. Direct MSI evidence does not qualify the actual
AppKit update confirmation, helper handoff or automatic reopen. Real provider
native-session acceptance also remains separate; the frozen QQQ RC.20 runtime
does not advertise the required API 1.9 grant. No 1.0 sign-off is claimed here.
This 0.3.1 checkpoint used a direct MSI installation. The subsequent real AppKit
transition to 0.3.2 is recorded above. Its new component-coherence guard passed
73 focused updater cases and the integrated 426-test Release gate. The frozen
QQQ RC.20 runtime does not advertise the required API 1.9 native-session grant.
## Installed published 0.2.0 and confirmed output — 2026-10-31
+61 -11
View File
@@ -1,8 +1,52 @@
# Session state — 2026-10-31
# Session state — 2026-11-01
## Installed 0.3.1 and 0.3.2 qualification — 2026-11-01
## Query in About — 2026-11-01
The published 0.3.1 MSI was installed with exit 0 and no reboot. App, service and
The shared AppKit About dialog now places Query and its public links in a compact
two-column section just after the brand header. The original header logo/text
are reparented into an Auto/Star Grid so long customer names wrap without
clipping; identity, version, license and credits remain supplied by AppKit.
The PNG is bundled unmodified with provenance on a 128-pixel white card.
The Release gate passes 426 tests (145 Core, 281 Windows/CLI), with zero build
warnings/errors. The isolated native host passed image decoding, rendered
contrast, initial mascot/link/name visibility, scrolling, keyboard focus and
Close checks in both themes with a long synthetic customer name at observed
125% DPI; full dialog captures verify the refinement. The private publish output contains
the exact PNG, provenance and required WinUI resources. These About changes are
not yet released or installed; the native host uses synthetic service responses.
## Published and installed 0.3.2 — 2026-11-01
[0.3.2-rc.1](https://github.com/QRun-IO/qqq-print-agent/releases/tag/v0.3.2-rc.1)
is published from protected main `268f951b6728b0e69fedaf46769785c3f5ae55f4`.
[Release workflow 41324312106](https://github.com/QRun-IO/qqq-print-agent/actions/runs/41324312106)
completed all nine jobs. The signed tag, all four assets, checksums and GitHub
provenance attestations were verified. Read-only MSI inspection passed for
product version `0.3.2`, coherent app/service/CLI ProductVersion
`0.3.2-rc.1+268f951b6728b0e69fedaf46769785c3f5ae55f4`, WinUI resources,
service/callback registration and packaged notices. Published MSI SHA-256:
`1191C337265469EB06136AF232DB57FAC6BC5014357521D80B35E5CCA5AF6E18`.
It remains unsigned by the existing prerelease decision.
The actual AppKit 0.3.1-to-0.3.2 update completed with MSI exit 0. The user chose
Help → Check for updates → Not now and confirmed continued responsiveness,
then Update and restart. The companion automatically reopened, and the user
confirmed Connected and both printers. Independent read-only observations
verified all three installed executable hashes and full versions against the
published MSI, the original companion/copied 0.3.1 helper handoff, an automatic
LocalSystem service and restored WebSocket. All 38 earlier local and 37 earlier
server job rows remained unchanged; one additional row in each view was
accounted for separately. Native identity, pairing, exposure and profiles were
preserved. The observers submitted no print jobs and did not retry the installer.
The diagnostic launcher's final summary failed after its child succeeded because
it reopened stdout while holding its writer. The original failed record is
preserved. Both original child reports passed and were independently revalidated,
including their hashes and preservation comparisons; the launcher itself is
not reported as passing. See [acceptance evidence](ACCEPTANCE.md).
Earlier, the published 0.3.1 MSI was installed with exit 0 and no reboot. App, service and
CLI product versions exactly match source `346fc285198908c47a2ee9db7b9adb53d5e88c3c`.
The service restored its native QQQ session and connected WebSocket. Pairing,
printer exposure/profiles, log policy and all 28 existing local job receipts
@@ -10,22 +54,28 @@ were preserved. A separate authenticated comparison proved that the original
27 server jobs remained unchanged while new jobs arrived. The operator confirmed
test prints and a responsive installed window.
The next candidate checks all three installed components and identical complete
The published 0.3.2 checks all three installed components and identical complete
build metadata after MSI success. Focused red/green regressions cover missing,
old, unreadable and mixed-source builds; 73 focused cases pass. The integrated
426-test Release gate passes with zero warnings/errors/skips. Actual in-app
coordinator handoff remains unqualified until the next published update is used.
426-test Release gate passes with zero warnings/errors/skips. The installed
0.3.1 helper performed this transition; the new guard protects subsequent
updates. Actual failed/canceled update, restart-required and reboot recovery
paths remain unqualified. Prior paper confirmation remains specific to 0.3.1.
The native test host now matches production's PerMonitorV2 declaration while
retaining asInvoker. Original 0.3.0 and current 0.3.1 UI replays passed at observed
retaining asInvoker. Original 0.3.0 and then-current 0.3.1 UI replays passed at observed
125% DPI, including repeated banner dismissal and printer status transitions.
These results and operator acceptance do not prove the historical cause fixed.
No speculative product layout change was made.
Local Docker development qualification is being added to the server repository.
The frozen QQQ RC.20 runtime lacks the advertised native-session grant required
for Windows OAuth. Real Authentik browser/session qualification needs the central
API 1.9 implementation; the client must keep its HTTPS and typed-grant checks.
The repository-owned local Docker reference environment passed real Authentik
login, permission checks, logout replay denial, server renewal after expiry and
provider revocation denial using production client session code. This isolated
proof uses unreleased QQQ API 1.9 candidate `ecf0f550` and an HTTPS probe callback,
with trust confined to containers. Released RC.20 lacks the required native grant.
Installed Windows browser/private-callback acceptance still needs a compatible
published QQQ runtime and properly configured HTTPS provider; no installed OAuth
or long-running production qualification is claimed.
## Installed realtime release and companion follow-up
+17
View File
@@ -0,0 +1,17 @@
# Owned source notice check
Run these commands from the repository root with the existing Python 3 interpreter:
```text
python3 .github/scripts/test-source-headers.py
python3 .github/scripts/check-source-headers.py
```
The standard-library checker enforces the scope of the reviewed 2026-11-01 header pass, not an estate-wide licensing or formatting audit. `.github/source-headers.json` names the owned source/config path families, comment syntax, accepted holder/license pairs and explicit exclusion reasons. Python `fnmatch` patterns match slash-containing relative Git paths; new files in those families are covered after they are tracked. Empty selections fail. The checker does not scan untracked build output or modify files.
Only leading language comments count. Copyright text in a string, Python docstring or comment after code is rejected. UTF-8 BOMs, initial shebangs, PowerShell `#Requires` directives and XML declarations are handled before the notice. These declarations cannot be displaced beneath a new notice. PowerShell block comments are recognized only for PowerShell files. Existing years and holders are not rewritten. Complete canonical Apache notices are valid without redundant SPDX; Java still requires the full Apache block. A contradictory SPDX identifier fails.
JSON/schema/fixture/lock data, explicit generated output directories, vendor legal assets and upstream Maven wrappers are excluded with reasons in the policy. Solution/activation/test-runner metadata and `.editorconfig` remain outside this completed pass; their applicable root LICENSE/NOTICE remains authoritative. No broad claim about third-party licensing follows from this check. New owned file types or provenance must be reviewed before changing the allowlist; do not add blanket exclusions to make a failure disappear.
The implementation and its negative tests are intentionally maintained copies across the three printing repositories, with each repository's own copyright/license header and independently reviewed scope. They do not fetch policy or execute source. CI invokes them in existing jobs using the runner's available Python, with no new package or workflow context. Existing native C# IDE0073 and Java Checkstyle conventions remain unchanged.
This client accepts James Maes / MIT in its reviewed source/build/config families and preserves QRun.IO, Inc. / MIT solely in tools/virtual-agent. The existing scripts / virtual-agent job runs the guard.
+14 -4
View File
@@ -1,19 +1,29 @@
# TODO
## Query in About
- [x] Place the official mascot and dogfooding/Query links directly below the
shared About header, preserving customer logos, identity and credits.
- [x] Verify both native themes, initial image/link visibility, unclipped names and
Close; pass the 426-test Release gate and exact publish-payload asset check.
- [ ] Package, publish and verify the About change in an installed follow-up.
## Companion layout, live transport status and startup follow-up
- [x] Install the attested published 0.3.1 MSI; verify all three exact component versions, automatic WebSocket restoration and preserved local receipts, pairing and profiles.
- [x] Record operator-confirmed test prints and responsive installed 0.3.1 window; verify repeated native dialog/refresh/banner/status replays at observed 125% DPI.
- [x] Reject a successful MSI outcome when the app, service or CLI has a missing, old or different full build version; pass the integrated 426-test gate.
- [ ] Qualify the actual AppKit in-app update to the next published candidate, including decline, helper handoff and reopen; direct MSI installation is separate evidence.
- [ ] Finish real-provider native-session qualification against a compatible QQQ API 1.9 runtime and the local Docker reference environment.
- [x] Publish [0.3.2-rc.1](https://github.com/QRun-IO/qqq-print-agent/releases/tag/v0.3.2-rc.1); verify all nine [release jobs](https://github.com/QRun-IO/qqq-print-agent/actions/runs/41324312106), four assets/attestations and static MSI version, resources, service/callback registration and notices. The MSI remains unsigned.
- [x] Qualify the actual AppKit 0.3.1-to-0.3.2 update: operator-confirmed Not now/responsiveness, Update and restart, verified old-helper handoff, MSI exit 0, exact published component bytes, preserved identity/profiles/earlier jobs, restored WebSocket and automatic visible reopen.
- [x] Pass isolated real Authentik native-session login, permission, logout, expiry/renewal and revocation checks with production client code against unreleased QQQ API 1.9 candidate `ecf0f550` in the local Docker reference environment.
- [ ] Qualify the installed Windows browser/private callback against a compatible published QQQ runtime and configured HTTPS provider; the container HTTPS callback does not establish this installed flow.
- [x] Publish and verify all three 0.3.0-rc.1 releases; install the matched local server/client and verify retained state and service restart recovery.
- [x] Capture the operator's post-print LayoutCycleException and recover only the companion; retain existing print receipts.
- [ ] Reproduce and fix the native post-print layout cycle with a nonprinting host and verify the actual dialog/action/refresh path.
- [x] Verify actual native fatal-layout exception logging, handler order and natural diagnostic termination; keep this mitigation distinct from fixing the original trigger.
- [x] Accept scheduler-normalized account names only when they resolve to the exact expected startup SID; verify rejection of other identities and the unchanged actual task.
- [x] Show explicit WebSocket state, server endpoint, live upload/download rates and service-lifetime application-traffic totals in the footer; verify actual native presentation with synthetic traffic.
- [ ] Publish and install the verified client follow-up, then complete UI and remaining physical acceptance without resubmitting old intents.
- [x] Preserve the original diagnostic launcher's finalization failure and independently validate both passing child reports; no installer retry or observer print was needed. Keep 0.3.1 physical acceptance separate.
## Realtime and Jobs UI — 0.3.0-rc.1 release candidate
@@ -109,7 +119,7 @@ The following entries preserve earlier implementation checkpoints.
## Remaining installed qualification
- [ ] Qualify real in-app update: consent, failed/canceled download or elevation, retained state, helper handoff, coherent versions, relaunch and restart-required reporting.
- [ ] Qualify actual update download/installer failure and cancellation, restart-required reporting and reboot recovery; the successful 0.3.2 update above does not establish these cases.
- [ ] Review completed protected update stages/logs manually; no automatic completed-stage retention policy is implemented.
- [ ] Qualify remaining input-dependent selection/focus, long-value and metadata synchronization scenarios; passing UI checks do not cover every interaction.
- [ ] Qualify installed credential recovery, queued/retry/cancel job controls, stale service state and bounded SCM recovery.
+19
View File
@@ -0,0 +1,19 @@
# Query artwork
`query-mascot.png` is the official QQQ and QRun.IO dog mascot, supplied by
QRun.IO and bundled unmodified for the About dialog.
- Source: [QRun-IO/marketing-website](https://github.com/QRun-IO/marketing-website),
revision `5ff7c1564f4fe28a7ce75436cf7c4a6eeaa4bf3f`.
- PNG: [`assets/Puppy Mark/exports/PNG/qqq-puppy@4x.png`](https://github.com/QRun-IO/marketing-website/blob/5ff7c1564f4fe28a7ce75436cf7c4a6eeaa4bf3f/assets/Puppy%20Mark/exports/PNG/qqq-puppy%404x.png).
- Related website artwork: `public/images/qqq-puppy.svg` at the same revision.
- Dimensions: 1326 × 1394 pixels, transparent PNG.
- SHA-256: `385a053e15e0a9fc6f8a965257e3917db827594ab46ecf8cdddb847770a25dbb`.
The About dialog scales the image uniformly on a white card. It does not
download or alter the artwork, replace customer branding, or change the
application and tray icons. Query remains QRun.IO/QQQ branding; this provenance
note does not grant separate trademark permissions or relicense the artwork.
Public pages: [dogfooding](https://www.qrun.io/dogfooding) and
[Query](https://www.qrun.io/query).
Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

+1
View File
@@ -662,6 +662,7 @@ public sealed partial class MainWindow : Window, IDisposable
{
body.Children.Insert(0, new Image { Source = CustomerLogo.Source, Width = 128, Height = 64, Stretch = Stretch.Uniform, HorizontalAlignment = HorizontalAlignment.Left });
}
QueryAboutSection.InsertAfterHeader(body);
}
await DialogGuard.ShowAsync(dialog);
}
+1
View File
@@ -29,6 +29,7 @@
<Content Include="Assets\Brand\app-icon.svg" CopyToOutputDirectory="PreserveNewest" />
<Content Include="Assets\Brand\app.ico" CopyToOutputDirectory="PreserveNewest" />
<Content Include="Assets\Brand\tray-*.png" CopyToOutputDirectory="PreserveNewest" />
<Content Include="Assets\Query\query-mascot.png;Assets\Query\README.md" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
<Manifest Include="$(ApplicationManifest)" />
</ItemGroup>
+113
View File
@@ -0,0 +1,113 @@
/*
* Copyright (c) 2026 James Maes
* SPDX-License-Identifier: MIT
*/
using Microsoft.UI;
using Microsoft.UI.Text;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Automation;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Media.Imaging;
namespace Qqq.PrintAgent;
/// <summary>
/// Adds QQQ's mascot and public project links without replacing AppKit's identity or credits.
/// </summary>
internal static class QueryAboutSection
{
/// <summary>Places Query after the brand header while retaining any preceding customer logo and all existing sections.</summary>
internal static Grid InsertAfterHeader(StackPanel body)
{
Border header = body.Children.OfType<Border>().First();
ConstrainHeader(header);
Grid section = Create();
body.Children.Insert(body.Children.IndexOf(header) + 1, section);
return section;
}
/// <summary>Gives the original name a finite width without replacing AppKit's logo, text, fonts or gradient.</summary>
private static void ConstrainHeader(Border header)
{
if(header.Child is not StackPanel { Children.Count: > 0 } content
|| content.Children[0] is not StackPanel { Orientation: Orientation.Horizontal, Children.Count: 2 } title
|| title.Children[0] is not Image logo || title.Children[1] is not TextBlock name)
{
return;
}
var row = new Grid { ColumnSpacing = title.Spacing };
row.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
row.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
title.Children.Clear();
name.TextWrapping = TextWrapping.Wrap;
Grid.SetColumn(name, 1);
row.Children.Add(logo);
row.Children.Add(name);
content.Children[0] = row;
}
/// <summary>Keeps the mascot and wrapped links together in a compact native layout.</summary>
private static Grid Create()
{
var text = new StackPanel { Spacing = 4, VerticalAlignment = VerticalAlignment.Center };
text.Children.Add(new TextBlock { Text = "Query", FontSize = 20, FontWeight = FontWeights.SemiBold });
text.Children.Add(new TextBlock { Text = "QQQ and QRun.IO’s dog mascot.", TextWrapping = TextWrapping.Wrap });
text.Children.Add(Link("Dogfooding at QRun.IO", "https://www.qrun.io/dogfooding", "AboutDogfoodingLink"));
text.Children.Add(Link("Meet Query", "https://www.qrun.io/query", "AboutQueryLink"));
var section = new Grid { ColumnSpacing = 12 };
section.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
section.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
Grid.SetColumn(text, 1);
section.Children.Add(CreateCard());
section.Children.Add(text);
return section;
}
/// <summary>Keeps the original transparent black artwork visible in either application theme.</summary>
private static Border CreateCard()
{
var image = new Image
{
Source = new BitmapImage(new Uri("ms-appx:///Assets/Query/query-mascot.png")),
Stretch = Stretch.Uniform,
};
AutomationProperties.SetName(image, "Query, the QQQ and QRun.IO dog mascot");
AutomationProperties.SetAutomationId(image, "AboutQueryMascot");
return new Border
{
Width = 128,
Height = 128,
Padding = new Thickness(8),
CornerRadius = new CornerRadius(8),
HorizontalAlignment = HorizontalAlignment.Left,
Background = new SolidColorBrush(Colors.White),
Child = image,
};
}
/// <summary>Uses native hyperlink navigation so links remain keyboard and screen-reader accessible.</summary>
private static HyperlinkButton Link(string label, string address, string automationId)
{
var link = new HyperlinkButton
{
Content = new TextBlock { Text = label, TextWrapping = TextWrapping.Wrap },
NavigateUri = new Uri(address),
Padding = new Thickness(0),
HorizontalAlignment = HorizontalAlignment.Stretch,
HorizontalContentAlignment = HorizontalAlignment.Left,
};
AutomationProperties.SetAutomationId(link, automationId);
return link;
}
}
@@ -46,6 +46,7 @@ internal static class LayoutScenario
await Task.Delay(250);
root.UpdateLayout();
await VerifyFooterAsync(root, service);
await QueryAboutScenario.RunAsync(root);
var feedback = (InfoBar)root.FindName("FeedbackBar");
if(!feedback.IsOpen || !string.Equals(feedback.Title, "Request completed", StringComparison.Ordinal))
{
@@ -63,6 +64,7 @@ internal static class LayoutScenario
service.TransportReads,
trafficRateDecayVerified = true,
serverLinkOpensSettingsOnly = true,
queryAboutNativeRenderingVerified = true,
heldForTimerRefresh = Program.RefreshDuringDialog,
width = root.XamlRoot.Size.Width,
height = root.XamlRoot.Size.Height,
@@ -23,6 +23,7 @@
<ApplicationDefinition Include="../../src/Qqq.PrintAgent/App.xaml" Link="App.xaml" />
<Page Include="../../src/Qqq.PrintAgent/MainWindow.xaml" Link="MainWindow.xaml" />
<Content Include="../../src/Qqq.PrintAgent/Assets/Brand/*" Link="Assets/Brand/%(Filename)%(Extension)" CopyToOutputDirectory="PreserveNewest" />
<Content Include="../../src/Qqq.PrintAgent/Assets/Query/*" Link="Assets/Query/%(Filename)%(Extension)" CopyToOutputDirectory="PreserveNewest" />
<Using Include="Qqq.PrintAgent.Core" />
<ProjectReference Include="../../src/Qqq.PrintAgent.Core/Qqq.PrintAgent.Core.csproj" />
<ProjectReference Include="../../src/Qqq.PrintAgent.Windows/Qqq.PrintAgent.Windows.csproj" />
@@ -0,0 +1,223 @@
/*
* Copyright (c) 2026 James Maes
* SPDX-License-Identifier: MIT
*/
using System.Text.Json;
using KofTwentyTwo.AppKit;
using KofTwentyTwo.AppKit.WinUI;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Automation;
using Microsoft.UI.Xaml.Automation.Peers;
using Microsoft.UI.Xaml.Automation.Provider;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Media.Imaging;
using Windows.Foundation;
using Windows.Graphics.Imaging;
using Windows.Storage;
using Windows.Storage.Streams;
namespace Qqq.PrintAgent;
/// <summary>Exercises the bundled image and scrollable shared dialog on a real, isolated WinUI surface.</summary>
internal static class QueryAboutScenario
{
/// <summary>Checks both themes with long customer identity text without opening a browser or service connection.</summary>
internal static async Task RunAsync(FrameworkElement root)
{
AppInfo identity = AppIdentity.DefaultInfo with
{
DisplayName = "North Distribution Centre — Customer Warehouse Printing Operations",
Description = "A long customer description remains in the shared About dialog together with the version, license and existing component credits.",
};
foreach(ElementTheme theme in new[] { ElementTheme.Light, ElementTheme.Dark })
{
var dialog = new AboutDialog(identity) { XamlRoot = root.XamlRoot, RequestedTheme = theme };
var scroll = (ScrollViewer)dialog.Content;
var body = (StackPanel)scroll.Content;
UIElement[] originalContent = body.Children.ToArray();
Grid section = QueryAboutSection.InsertAfterHeader(body);
Border card = section.Children.OfType<Border>().Single();
var image = (Image)card.Child;
bool imageOpened = false;
bool imageFailed = false;
image.ImageOpened += (_, _) => imageOpened = true;
image.ImageFailed += (_, _) => imageFailed = true;
Task<ContentDialogResult?> showing = DialogGuard.ShowAsync(dialog);
try
{
await WaitAsync(() => imageOpened || imageFailed);
VerifyImage(image, imageFailed);
if(!body.Children.Where(element => !ReferenceEquals(element, section)).SequenceEqual(originalContent))
{
throw new InvalidOperationException("The existing About content changed.");
}
await VerifyOpeningAsync(dialog, scroll, section, identity.DisplayName, theme);
HyperlinkButton query = Descendants(section).OfType<HyperlinkButton>().Last();
Rect bounds = query.TransformToVisual(scroll).TransformBounds(new Rect(0, 0, query.ActualWidth, query.ActualHeight));
if(bounds.Top < -1 || bounds.Bottom > scroll.ViewportHeight + 1
|| bounds.Left < -1 || bounds.Right > scroll.ViewportWidth + 1 || !query.Focus(FocusState.Keyboard)
|| !string.Equals(query.NavigateUri.AbsoluteUri, "https://www.qrun.io/query", StringComparison.Ordinal))
{
throw new InvalidOperationException("The Query link was not keyboard reachable within the native scroll viewport.");
}
HyperlinkButton dogfooding = Descendants(section).OfType<HyperlinkButton>().First();
if(!string.Equals(dogfooding.NavigateUri.AbsoluteUri, "https://www.qrun.io/dogfooding", StringComparison.Ordinal)
|| string.IsNullOrWhiteSpace(AutomationProperties.GetName(image)))
{
throw new InvalidOperationException("The public links or mascot accessibility description were unavailable.");
}
await CaptureAsync(card, $"about-query-{theme}.png", requireContrast: true);
scroll.ChangeView(null, scroll.ScrollableHeight, null, disableAnimation: true);
await WaitAsync(() => scroll.VerticalOffset > 0 && Math.Abs(scroll.VerticalOffset - scroll.ScrollableHeight) < 1);
if(!VisibleIn((FrameworkElement)body.Children.Last(), scroll))
{
throw new InvalidOperationException("The original AppKit credits were not available after scrolling.");
}
CloseThroughButton(dialog);
await showing;
}
finally
{
dialog.Hide();
await showing;
}
}
}
/// <summary>Requires a decoded bundled asset rather than a correctly named but missing resource.</summary>
private static void VerifyImage(Image image, bool failed)
{
if(failed || image.ActualWidth <= 0 || image.ActualHeight <= 0
|| image.Source is not BitmapImage { PixelWidth: 1326, PixelHeight: 1394 })
{
throw new InvalidOperationException("The native resource loader did not decode the official mascot dimensions.");
}
}
/// <summary>Captures the initial native layout before scrolling can hide a clipped name or an offscreen link.</summary>
private static async Task VerifyOpeningAsync(ContentDialog dialog, ScrollViewer scroll, FrameworkElement section, string displayName, ElementTheme theme)
{
dialog.UpdateLayout();
TextBlock name = Descendants(dialog).OfType<TextBlock>().Single(text => string.Equals(text.Text, displayName, StringComparison.Ordinal));
FrameworkElement[] required = Descendants(section).OfType<FrameworkElement>()
.Where(element => element is Image or HyperlinkButton).ToArray();
bool queryVisible = required.Length == 3 && required.All(element => VisibleIn(element, scroll));
bool nameVisible = VisibleIn(name, scroll) && !name.IsTextTrimmed && name.DesiredSize.Width <= name.ActualWidth + 1;
File.WriteAllText(Path.Combine(Program.EvidenceDirectory, $"about-initial-{theme}.json"), JsonSerializer.Serialize(new
{
queryVisible,
nameVisible,
nameWidth = name.ActualWidth,
nameHeight = name.ActualHeight,
scroll.ViewportWidth,
scroll.ViewportHeight,
scroll.VerticalOffset,
}));
await CaptureAsync(dialog, $"about-dialog-{theme}.png", requireContrast: false);
if(!queryVisible || !nameVisible || scroll.VerticalOffset != 0)
{
throw new InvalidOperationException("The initial About viewport clipped its customer name, mascot or public links.");
}
}
/// <summary>Checks the actual arranged rectangle, rather than trusting wrapping or visibility properties.</summary>
private static bool VisibleIn(FrameworkElement element, ScrollViewer scroll)
{
Rect bounds = element.TransformToVisual(scroll).TransformBounds(new Rect(0, 0, element.ActualWidth, element.ActualHeight));
return bounds.Top >= -1 && bounds.Left >= -1 && bounds.Bottom <= scroll.ViewportHeight + 1
&& bounds.Right <= scroll.ViewportWidth + 1 && bounds.Width > 0 && bounds.Height > 0;
}
/// <summary>Checks optional card contrast and retains only synthetic native-dialog pixels for visual review.</summary>
private static async Task CaptureAsync(FrameworkElement target, string fileName, bool requireContrast)
{
var rendering = new RenderTargetBitmap();
await rendering.RenderAsync(target);
IBuffer buffer = await rendering.GetPixelsAsync();
byte[] pixels = new byte[checked((int)buffer.Length)];
using(var reader = DataReader.FromBuffer(buffer))
{
reader.ReadBytes(pixels);
}
int white = 0;
int black = 0;
for(int index = 0; index < pixels.Length; index += 4)
{
if(pixels[index + 3] == byte.MaxValue)
{
if(pixels[index] > 245 && pixels[index + 1] > 245 && pixels[index + 2] > 245)
{
white++;
}
if(pixels[index] < 32 && pixels[index + 1] < 32 && pixels[index + 2] < 32)
{
black++;
}
}
}
if(requireContrast && (white < pixels.Length / 8 || black < 20))
{
throw new InvalidOperationException("The rendered Query card did not retain visible black artwork on white.");
}
StorageFolder folder = await StorageFolder.GetFolderFromPathAsync(Program.EvidenceDirectory);
StorageFile file = await folder.CreateFileAsync(fileName, CreationCollisionOption.FailIfExists);
using IRandomAccessStream output = await file.OpenAsync(FileAccessMode.ReadWrite);
BitmapEncoder encoder = await BitmapEncoder.CreateAsync(BitmapEncoder.PngEncoderId, output);
encoder.SetPixelData(BitmapPixelFormat.Bgra8, BitmapAlphaMode.Premultiplied,
checked((uint)rendering.PixelWidth), checked((uint)rendering.PixelHeight), 96, 96, pixels);
await encoder.FlushAsync();
}
/// <summary>Uses the framework's actual close action after scrolling instead of assuming its template stays usable.</summary>
private static void CloseThroughButton(ContentDialog dialog)
{
Button close = Descendants(dialog).OfType<Button>().Single(button => string.Equals(button.Name, "CloseButton", StringComparison.Ordinal));
AutomationPeer peer = FrameworkElementAutomationPeer.CreatePeerForElement(close)
?? throw new InvalidOperationException("The shared About close button has no native automation peer.");
if(!close.Focus(FocusState.Keyboard) || peer.GetPattern(PatternInterface.Invoke) is not IInvokeProvider invoke)
{
throw new InvalidOperationException("The shared About close button was not keyboard accessible.");
}
invoke.Invoke();
}
/// <summary>Finds realized native controls without external input or desktop automation.</summary>
private static IEnumerable<DependencyObject> Descendants(DependencyObject root)
{
for(int index = 0; index < VisualTreeHelper.GetChildrenCount(root); index++)
{
DependencyObject child = VisualTreeHelper.GetChild(root, index);
yield return child;
foreach(DependencyObject descendant in Descendants(child))
{
yield return descendant;
}
}
}
/// <summary>Fails the isolated host promptly when a native image or scrolling event never completes.</summary>
private static async Task WaitAsync(Func<bool> condition)
{
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5));
while(!condition())
{
await Task.Delay(25, timeout.Token);
}
}
}
@@ -26,4 +26,12 @@ No real printing, connection enrollment, settings commits, service restart, star
The footer check uses the real `ApplicationTrafficMeter` with synthetic byte increments, verifies positive rates followed by zero after an idle window while totals remain unchanged, and invokes the actual server-link peer. It requires the existing connection dialog's native Opened/Closed events and a null NavigateUri; cleanup uses ContentDialog.Hide. It does not sign in or navigate to an external website.
The About check renders the shared AppKit dialog with a long synthetic customer
name and the production Query section in both light and dark themes. It requires
the bundled PNG to decode and verifies rendered black artwork on white. The
mascot, both links and full customer name must fit the opening viewport before
scrolling; the original credits and Close action remain reachable afterward.
It retains full synthetic dialog and card captures for review, without
activating the website links.
This is a manual native desktop gate. It has no external input or printing dependency, but it is not a headless unit test and has not been qualified on a GitHub-hosted runner. It requires PowerShell 7, the repository's .NET 10 build prerequisites and a working WinUI desktop. The native Windows App SDK components are copied by the build; the executable still uses the installed .NET runtime. No mandatory CI step is added until that runner environment is independently qualified.
+1
View File
@@ -61,6 +61,7 @@ try {
$result = Get-Content -LiteralPath (Join-Path $evidence 'result.json') -Raw | ConvertFrom-Json
if(-not $result.passed -or $result.Submissions -ne 1 -or $result.DialogSnapshots -lt 1 -or
-not $result.trafficRateDecayVerified -or -not $result.serverLinkOpensSettingsOnly -or
-not $result.queryAboutNativeRenderingVerified -or
(Test-Path -LiteralPath (Join-Path $evidence 'unhandled.txt'))) {
throw 'The native host did not provide complete passing evidence.'
}