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
64 changes: 58 additions & 6 deletions app/desktop/scripts/manual-chat-flow-check.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ const ownsServer = !process.env.AGENTHUB_MANUAL_URL;
const baseURL = process.env.AGENTHUB_MANUAL_URL ?? `http://127.0.0.1:${port}`;
const outputDir = path.resolve(process.cwd(), '.tmp', 'manual-chat-flow-uiux');
const screenshot = path.join(outputDir, 'desktop-1440x810-chat-flow.png');
const metricsPath = path.join(outputDir, 'desktop-1440x810-chat-flow.metrics.json');
const reportPath = path.join(outputDir, 'desktop-chat-flow-visual-qa.json');

fs.mkdirSync(outputDir, { recursive: true });

Expand Down Expand Up @@ -125,6 +127,48 @@ function assertMetrics(result) {
}
}

function writeVisualQaReport({ status, viewport, metrics, failure }) {
const report = {
schema: 'agenthub.chat_visual_qa.v1',
surface: 'desktop',
status,
evidence_level: 'visual-qa',
real_tested: false,
generated_at: new Date().toISOString(),
baseURL,
viewport,
screenshot,
metricsPath,
reportPath,
metrics,
failure: failure ? { message: failure } : null,
inspection: {
screenshot,
metricsPath,
checks: [
'Confirm the transcript stays in chronological order.',
'Confirm user bubbles do not disappear during optimistic send.',
'Confirm approval and preview cards render as one clean stack.',
'Confirm no mock, debug, or data-mode text appears inside the transcript.',
],
},
boundaries: {
real_tokendance_id_login: false,
real_cli_or_model_api: false,
packaged_desktop: false,
signing: false,
release_upload: false,
production_deploy: false,
},
};
fs.writeFileSync(metricsPath, `${JSON.stringify(metrics, null, 2)}\n`);
fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`);
console.log(`Visual QA screenshot: ${screenshot}`);
console.log(`Visual QA metrics: ${metricsPath}`);
console.log(`Visual QA report: ${reportPath}`);
return report;
}

let server = null;
let browser = null;

Expand Down Expand Up @@ -283,14 +327,22 @@ try {
};
}, { firstMessage, secondMessage });

const report = {
baseURL,
let failure = null;
try {
assertMetrics(result);
} catch (error) {
failure = error instanceof Error ? error.message : String(error);
}
const report = writeVisualQaReport({
status: failure ? 'failed' : 'passed',
viewport: page.viewportSize(),
screenshot,
...result,
};
metrics: result,
failure,
});
console.log(JSON.stringify(report, null, 2));
assertMetrics(result);
if (failure) {
throw new Error(failure);
}
} finally {
if (browser) await browser.close();
stopDevServer(server);
Expand Down
67 changes: 59 additions & 8 deletions app/web/scripts/manual-chat-flow-check.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ const baseURL = process.env.AGENTHUB_WEB_MANUAL_URL ?? `http://127.0.0.1:${port}
const hubOrigin = 'http://localhost:8080';
const outputDir = path.resolve(process.cwd(), '.tmp', 'manual-chat-flow-uiux');
const screenshot = path.join(outputDir, 'web-1440x810-chat-flow.png');
const metricsPath = path.join(outputDir, 'web-1440x810-chat-flow.metrics.json');
const reportPath = path.join(outputDir, 'web-chat-flow-visual-qa.json');
const sessionId = 'session-web-manual-chat-flow';
const taskId = 'task-web-manual-chat-flow';

Expand Down Expand Up @@ -294,6 +296,49 @@ function assertMetrics(result) {
}
}

function writeVisualQaReport({ status, viewport, metrics, failure }) {
const report = {
schema: 'agenthub.chat_visual_qa.v1',
surface: 'web',
status,
evidence_level: 'visual-qa',
real_tested: false,
generated_at: new Date().toISOString(),
baseURL,
dataSource: 'stubbed-hub-session',
viewport,
screenshot,
metricsPath,
reportPath,
metrics,
failure: failure ? { message: failure } : null,
inspection: {
screenshot,
metricsPath,
checks: [
'Confirm transcript order is user, tool result, then agent reply.',
'Confirm Markdown tables render in the transcript.',
'Confirm subagent report details stay in the inspector.',
'Confirm no mock, debug, or data-mode text appears inside the transcript.',
],
},
boundaries: {
real_tokendance_id_login: false,
real_cli_or_model_api: false,
packaged_desktop: false,
signing: false,
release_upload: false,
production_deploy: false,
},
};
fs.writeFileSync(metricsPath, `${JSON.stringify(metrics, null, 2)}\n`);
fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`);
console.log(`Visual QA screenshot: ${screenshot}`);
console.log(`Visual QA metrics: ${metricsPath}`);
console.log(`Visual QA report: ${reportPath}`);
return report;
}

let server = null;
let browser = null;

Expand Down Expand Up @@ -410,16 +455,22 @@ try {
};
});

const report = {
baseURL,
let failure = null;
try {
assertMetrics(result);
} catch (error) {
failure = error instanceof Error ? error.message : String(error);
}
const report = writeVisualQaReport({
status: failure ? 'failed' : 'passed',
viewport: page.viewportSize(),
screenshot,
dataSource: 'stubbed-hub-session',
real_tested: false,
...result,
};
metrics: result,
failure,
});
console.log(JSON.stringify(report, null, 2));
assertMetrics(result);
if (failure) {
throw new Error(failure);
}
} finally {
if (browser) await browser.close();
stopDevServer(server);
Expand Down
1 change: 1 addition & 0 deletions docs/progress/MASTER.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,3 +116,4 @@ Per-task telemetry is stored in GitHub issue comments before task closure. Adapt
| 2026-06-29 | Phase 3 complete | #388 merged via #410 and closed manually because non-default base did not auto-close it; milestone #19 is closed at 3/3 with adaptive drift_score 2, so Phase 4 requires a lightweight checkpoint before T4.1. |
| 2026-06-29 | T4.1 implementation | Added a Node-based focused chat acceptance bundle for shared unit, Desktop/Web Playwright, and Desktop/Web Visual QA; package entry passed with `real_tested=false`; PR pending. |
| 2026-06-29 | Phase 4 sync | #389 merged via #412 and closed manually because non-default base did not auto-close it; milestone #20 is 1/3 complete with drift_score 1, and #390 is the active next task with a drift warning. |
| 2026-06-29 | T4.2 implementation | Added Desktop/Web Visual QA metrics and report artifacts, locked their paths in the chat acceptance manifest, and verified the full chat acceptance bundle with `real_tested=false`; PR pending. |
12 changes: 10 additions & 2 deletions scripts/verify/chat-acceptance.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,11 @@ await runGate({
cwd: path.join(repoRoot, 'app', 'desktop'),
command: corepack,
args: ['pnpm', '--dir', path.join(repoRoot, 'app', 'desktop'), 'run', 'test:visual:chat-flow'],
artifacts: ['app/desktop/.tmp/manual-chat-flow-uiux/desktop-1440x810-chat-flow.png'],
artifacts: [
'app/desktop/.tmp/manual-chat-flow-uiux/desktop-1440x810-chat-flow.png',
'app/desktop/.tmp/manual-chat-flow-uiux/desktop-1440x810-chat-flow.metrics.json',
'app/desktop/.tmp/manual-chat-flow-uiux/desktop-chat-flow-visual-qa.json',
],
skip: args.skipDesktopVisualQa,
skipReason: 'skipped by --skip-desktop-visual-qa',
});
Expand All @@ -92,7 +96,11 @@ await runGate({
cwd: path.join(repoRoot, 'app', 'web'),
command: corepack,
args: ['pnpm', '--dir', path.join(repoRoot, 'app', 'web'), 'run', 'test:visual:chat-flow'],
artifacts: ['app/web/.tmp/manual-chat-flow-uiux/web-1440x810-chat-flow.png'],
artifacts: [
'app/web/.tmp/manual-chat-flow-uiux/web-1440x810-chat-flow.png',
'app/web/.tmp/manual-chat-flow-uiux/web-1440x810-chat-flow.metrics.json',
'app/web/.tmp/manual-chat-flow-uiux/web-chat-flow-visual-qa.json',
],
skip: args.skipWebVisualQa,
skipReason: 'skipped by --skip-web-visual-qa',
});
Expand Down
4 changes: 4 additions & 0 deletions tests/contract/scripts/verify-chat-acceptance.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ try {
assert(json.rows.filter((row) => row.name === 'web-chat-playwright' && row.evidence_level === 'playwright-ui').length === 1, 'manifest has Web Playwright row');
assert(json.rows.filter((row) => row.name === 'desktop-chat-visual-qa' && row.evidence_level === 'visual-qa').length === 1, 'manifest has Desktop Visual QA row');
assert(json.rows.filter((row) => row.name === 'web-chat-visual-qa' && row.evidence_level === 'visual-qa').length === 1, 'manifest has Web Visual QA row');
assert(json.rows.find((row) => row.name === 'desktop-chat-visual-qa')?.artifacts?.includes('app/desktop/.tmp/manual-chat-flow-uiux/desktop-1440x810-chat-flow.metrics.json'), 'Desktop Visual QA row includes metrics artifact');
assert(json.rows.find((row) => row.name === 'desktop-chat-visual-qa')?.artifacts?.includes('app/desktop/.tmp/manual-chat-flow-uiux/desktop-chat-flow-visual-qa.json'), 'Desktop Visual QA row includes report artifact');
assert(json.rows.find((row) => row.name === 'web-chat-visual-qa')?.artifacts?.includes('app/web/.tmp/manual-chat-flow-uiux/web-1440x810-chat-flow.metrics.json'), 'Web Visual QA row includes metrics artifact');
assert(json.rows.find((row) => row.name === 'web-chat-visual-qa')?.artifacts?.includes('app/web/.tmp/manual-chat-flow-uiux/web-chat-flow-visual-qa.json'), 'Web Visual QA row includes report artifact');
assert(!/sk-[A-Za-z0-9]|client_secret|Authorization:\s*Bearer/.test(jsonText), 'manifest is redacted');
}

Expand Down
87 changes: 87 additions & 0 deletions tests/contract/scripts/verify-chat-visual-qa.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
#!/usr/bin/env node

import fs from 'node:fs';
import path from 'node:path';
import process from 'node:process';

const repoRoot = path.resolve(parseRepoRoot(process.argv.slice(2)));
const desktopScriptPath = path.join(repoRoot, 'app', 'desktop', 'scripts', 'manual-chat-flow-check.mjs');
const webScriptPath = path.join(repoRoot, 'app', 'web', 'scripts', 'manual-chat-flow-check.mjs');
const acceptancePath = path.join(repoRoot, 'scripts', 'verify', 'chat-acceptance.mjs');
let failed = 0;

assertVisualScriptContract('Desktop', desktopScriptPath, {
surface: 'desktop',
screenshot: 'desktop-1440x810-chat-flow.png',
metrics: 'desktop-1440x810-chat-flow.metrics.json',
report: 'desktop-chat-flow-visual-qa.json',
});

assertVisualScriptContract('Web', webScriptPath, {
surface: 'web',
screenshot: 'web-1440x810-chat-flow.png',
metrics: 'web-1440x810-chat-flow.metrics.json',
report: 'web-chat-flow-visual-qa.json',
});

assertAcceptanceContract(acceptancePath);

process.exit(failed > 0 ? 1 : 0);

function assertVisualScriptContract(label, scriptPath, expected) {
assert(fs.existsSync(scriptPath), `${label} Visual QA script exists`);
if (!fs.existsSync(scriptPath)) return;

const script = fs.readFileSync(scriptPath, 'utf8');
assert(script.includes(expected.screenshot), `${label} script keeps stable screenshot path`);
assert(script.includes(expected.metrics), `${label} script writes stable metrics path`);
assert(script.includes(expected.report), `${label} script writes stable report path`);
assert(script.includes('agenthub.chat_visual_qa.v1'), `${label} script writes stable report schema`);
assert(script.includes(`surface: '${expected.surface}'`), `${label} report records ${expected.surface} surface`);
assert(script.includes('metricsPath'), `${label} report includes metricsPath`);
assert(script.includes('reportPath'), `${label} report includes reportPath`);
assert(script.includes('inspection'), `${label} report includes concise inspection instructions`);
assert(script.includes('real_tested: false'), `${label} report records real_tested=false`);
assert(script.includes('evidence_level: \'visual-qa\''), `${label} report records visual-qa evidence level`);
assert(script.includes('fs.writeFileSync(metricsPath'), `${label} script persists metrics JSON`);
assert(script.includes('fs.writeFileSync(reportPath'), `${label} script persists report JSON`);
assert(script.includes('Visual QA screenshot:'), `${label} stdout prints screenshot path`);
assert(script.includes('Visual QA metrics:'), `${label} stdout prints metrics path`);
assert(script.includes('Visual QA report:'), `${label} stdout prints report path`);
assert(!/real_tested:\s*true|approved-real.+real_tested:\s*true|pnpm\s+tauri\s+build/i.test(script), `${label} script does not claim approved-real or packaged Desktop evidence`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Block approved-real explicitly.

This regex still passes if the script starts emitting approved-real while keeping real_tested: false, so it does not actually enforce the stated scope restriction.

Suggested fix
-  assert(!/real_tested:\s*true|approved-real.+real_tested:\s*true|pnpm\s+tauri\s+build/i.test(script), `${label} script does not claim approved-real or packaged Desktop evidence`);
+  assert(!/real_tested:\s*true/i.test(script), `${label} script does not claim real_tested=true`);
+  assert(!/approved-real/i.test(script), `${label} script does not claim approved-real evidence`);
+  assert(!/pnpm\s+tauri\s+build/i.test(script), `${label} script does not claim packaged Desktop evidence`);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
assert(!/real_tested:\s*true|approved-real.+real_tested:\s*true|pnpm\s+tauri\s+build/i.test(script), `${label} script does not claim approved-real or packaged Desktop evidence`);
assert(!/real_tested:\s*true/i.test(script), `${label} script does not claim real_tested=true`);
assert(!/approved-real/i.test(script), `${label} script does not claim approved-real evidence`);
assert(!/pnpm\s+tauri\s+build/i.test(script), `${label} script does not claim packaged Desktop evidence`);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/contract/scripts/verify-chat-visual-qa.mjs` at line 51, The
verification in verify-chat-visual-qa.mjs only rejects approved-real when paired
with real_tested:true, so it can still slip through with real_tested:false.
Update the assertion in the script check to explicitly reject any occurrence of
approved-real in the script text, alongside the existing real_tested and pnpm
tauri build guards, so the scope restriction is enforced regardless of other
flags.

}

function assertAcceptanceContract(scriptPath) {
assert(fs.existsSync(scriptPath), 'chat acceptance runner exists');
if (!fs.existsSync(scriptPath)) return;

const script = fs.readFileSync(scriptPath, 'utf8');
for (const artifact of [
'desktop-1440x810-chat-flow.png',
'desktop-1440x810-chat-flow.metrics.json',
'desktop-chat-flow-visual-qa.json',
'web-1440x810-chat-flow.png',
'web-1440x810-chat-flow.metrics.json',
'web-chat-flow-visual-qa.json',
]) {
assert(script.includes(artifact), `chat acceptance manifest includes ${artifact}`);
}
}

function assert(condition, message) {
if (condition) {
console.log(`PASS: ${message}`);
return;
}
failed += 1;
console.error(`FAIL: ${message}`);
}

function parseRepoRoot(args) {
for (let i = 0; i < args.length; i += 1) {
if (args[i] === '--repo-root' || args[i] === '-RepoRoot') {
return args[i + 1];
}
}
return '.';
}
Loading