test(ql3): exercise actual panel client in Linux runtime gate

This commit is contained in:
whyour
2026-09-04 04:49:45 +08:00
parent e4ba5d405f
commit e684a6e64a
11 changed files with 423 additions and 72 deletions
@@ -10,6 +10,8 @@ const path = require('node:path');
const { createRequire } = require('node:module');
const { spawn } = require('node:child_process');
const { DatabaseSync } = require('node:sqlite');
const assert = require('node:assert/strict');
const { loadPanelClient } = require('./ql3-panel-run-control-live-client.cjs');
const TIMEOUT_MS = 45_000;
@@ -188,7 +190,10 @@ function insertTaskAndIdentity(
command: {
kind: 'argv',
file: '/bin/sh',
args: ['-c', 'trap "exit 0" TERM INT; while :; do sleep 1; done'],
args: [
'-c',
'trap "exit 0" TERM INT; printf "qinglong3-panel-live-log\\n"; while :; do sleep 1; done',
],
},
},
},
@@ -529,35 +534,44 @@ async function main(argv = process.argv.slice(2)) {
() => active.events.some((event) => event.event === 'listening'),
'Local API listener',
);
const startBody = JSON.stringify({
schema: 'qinglong/task-start@v1',
mutationId: '019f8700-0000-7000-8000-000000000004',
expectedRevision: seeded.definition.revision,
expectedContentDigest: seeded.definition.contentDigest,
});
const started = await request(
const unauthorized = await request(
port,
seeded.token,
'/api/v3/projects/default/tasks/live-cancellation-task/runs',
{
method: 'POST',
headers: {
'content-type': 'application/json',
'content-length': String(Buffer.byteLength(startBody)),
},
body: startBody,
},
'',
'/api/v3/projects/default/tasks/live-cancellation-task',
);
if (started.statusCode !== 202 || started.body.status !== 'accepted') {
fail(`task start was rejected: ${JSON.stringify(started)}`);
}
assert.equal(unauthorized.statusCode, 401);
const panel = loadPanelClient(
path.join(value.evidenceRoot, 'panel-client'),
port,
);
const capabilities = await panel.auth.discoverQingLong3(
'/api/v3/capabilities',
);
assert.equal(capabilities?.panel.runControl, 'task_run_v1');
assert.equal(panel.auth.setQingLong3Credential(seeded.token), true);
const client = panel.control.createPanelRunControl(
{
ql3: {
projectId: 'default',
taskId: 'live-cancellation-task',
},
},
capabilities,
);
const task = await client.readTask();
assert.equal(task.revision, seeded.definition.revision);
assert.equal(task.contentDigest, seeded.definition.contentDigest);
const start = client.prepareStart(task);
assert.equal(panel.requests.filter((r) => r.method === 'POST').length, 0);
const started = await start.execute();
assert.equal(started.status, 'accepted');
const running = await waitFor(() => {
const row = query(
databasePath,
`SELECT run.status, attempt.status AS attemptStatus, attempt.pid
FROM Runs AS run JOIN RunAttempts AS attempt ON attempt.run_id = run.id
WHERE run.id = ?`,
started.body.runId,
started.runId,
);
return row?.status === 'running' &&
row?.attemptStatus === 'running' &&
@@ -568,47 +582,39 @@ async function main(argv = process.argv.slice(2)) {
const taskPid = Number(running.pid);
const taskStartTicks = procStartTicks(taskPid);
const apiRssBytes = rssBytes(active.child.pid);
const cancellationBody = JSON.stringify({
schema: 'qinglong/run-cancellation@v1',
mutationId: 'local-live-cancellation-1',
});
const cancellationPath = `/api/v3/projects/default/runs/${started.body.runId}/cancellation`;
const requestOptions = {
method: 'POST',
headers: {
'content-type': 'application/json',
'content-length': String(Buffer.byteLength(cancellationBody)),
},
body: cancellationBody,
};
const accepted = await request(
port,
seeded.token,
cancellationPath,
requestOptions,
const page = await client.listRuns();
assert.equal(page.scanned, 1);
assert.equal(page.runs[0]?.id, started.runId);
let selected;
await waitFor(async () => {
selected = await client.readRun(started.runId);
return (await client.readLog(selected)).includes(
'qinglong3-panel-live-log',
);
}, 'panel reads running process log marker');
assert.equal(selected.status, 'running');
const cancellation = client.prepareCancel(selected);
panel.loseNextCancellationResponse();
await assert.rejects(
cancellation.execute(),
(error) => error.uncertain === true,
);
const replay = await request(
port,
seeded.token,
cancellationPath,
requestOptions,
const replay = await cancellation.execute();
assert.equal(replay.status, 'already_requested');
assert.equal(replay.runId, started.runId);
const cancelRequests = panel.requests.filter(
(r) => r.method === 'POST' && r.path.endsWith('/cancellation'),
);
if (accepted.statusCode !== 202 || accepted.body.status !== 'accepted') {
fail(`cancellation was rejected: ${JSON.stringify(accepted)}`);
}
if (
replay.statusCode !== 200 ||
replay.body.status !== 'already_requested'
) {
fail(`cancellation replay drifted: ${JSON.stringify(replay)}`);
}
assert.equal(cancelRequests.length, 2);
assert.equal(cancelRequests[0].body, cancelRequests[1].body);
assert.equal(cancelRequests[0].path, cancelRequests[1].path);
const terminal = await waitFor(() => {
const row = query(
databasePath,
`SELECT run.status, attempt.status AS attemptStatus
FROM Runs AS run JOIN RunAttempts AS attempt ON attempt.run_id = run.id
WHERE run.id = ?`,
started.body.runId,
started.runId,
);
return row?.status === 'cancelled' && row?.attemptStatus === 'cancelled'
? row
@@ -618,23 +624,25 @@ async function main(argv = process.argv.slice(2)) {
() => !sameProcessExists(taskPid, taskStartTicks),
'task process identity exit',
);
await waitFor(async () => {
const current = await client.readRun(started.runId);
return (await client.readLog(current)).includes(
'qinglong3-panel-live-log',
);
}, 'panel reads actual task log marker');
await stopApi(active);
active = startApi(executable, apiConfigPath);
await waitFor(
() => active.events.some((event) => event.event === 'listening'),
'restarted Local API listener',
);
const observed = await request(
port,
seeded.token,
`/api/v3/projects/default/runs/${started.body.runId}`,
const observed = await client.readRun(started.runId);
assert.equal(observed.status, 'cancelled');
assert.ok(
(await client.readLog(observed)).includes('qinglong3-panel-live-log'),
);
if (
observed.statusCode !== 200 ||
observed.body.run.status !== 'cancelled'
) {
fail(`restart observation drifted: ${JSON.stringify(observed)}`);
}
client.dispose();
panel.auth.clearQingLong3Credential();
await stopApi(active);
const facts = query(
databasePath,
@@ -643,8 +651,8 @@ async function main(argv = process.argv.slice(2)) {
(SELECT COUNT(*) FROM RunEvents WHERE run_id = ? AND type = 'run.cancelled') AS cancelledEvents,
(SELECT COUNT(*) FROM QingLong3SecurityAuditEvents WHERE operation_id = 'run.cancel' AND outcome = 'allowed') AS cancelAudits,
(SELECT integrity_check FROM pragma_integrity_check LIMIT 1) AS integrity`,
started.body.runId,
started.body.runId,
started.runId,
started.runId,
);
if (
facts.cancelEvents !== 1 ||
@@ -654,7 +662,7 @@ async function main(argv = process.argv.slice(2)) {
)
fail(`durable facts drifted: ${JSON.stringify(facts)}`);
const report = {
schemaVersion: 1,
schemaVersion: 2,
profile: value.profile,
platform: { os: 'linux', architecture: process.arch, procfs: true },
resourceEnvelope: {
@@ -675,6 +683,23 @@ async function main(argv = process.argv.slice(2)) {
restartObservedCancelled: true,
sqliteIntegrity: facts.integrity,
},
panelClient: {
authSourceSha256: panel.manifest.auth.sourceSha256,
controlSourceSha256: panel.manifest.control.sourceSha256,
unauthenticatedStatus: unauthorized.statusCode,
capabilityDiscovered: true,
runListed: true,
logMarkerObserved: true,
restartLogMarkerObserved: true,
cancellationResponseLost: true,
exactCancellationBodyReplay: true,
startPosts: panel.requests.filter(
(r) => r.method === 'POST' && r.path.endsWith('/runs'),
).length,
cancellationPosts: cancelRequests.length,
browserRendering: false,
ownerProvisioning: 'seeded_fixture',
},
qualification: {
evidenceClass: 'linux_virtualized_live_contract',
physicalDevice: false,
@@ -0,0 +1,125 @@
'use strict';
// Test tooling only: execute the shipped panel modules against a real loopback API.
// Neither TypeScript nor this VM adapter belongs in the product runtime artifact.
const assert = require('node:assert/strict');
const crypto = require('node:crypto');
const fs = require('node:fs');
const path = require('node:path');
const vm = require('node:vm');
const SOURCES = Object.freeze({
auth: 'src/utils/qinglong3.ts',
control: 'src/components/qinglong3/runControl.ts',
});
const digest = (value) =>
crypto.createHash('sha256').update(value).digest('hex');
function preparePanelClient(repository, directory) {
const ts = require('typescript');
fs.mkdirSync(directory, { mode: 0o700 });
const manifest = {};
for (const [name, relative] of Object.entries(SOURCES)) {
const source = fs.readFileSync(path.join(repository, relative), 'utf8');
const compiled = ts.transpileModule(source, {
compilerOptions: {
target: ts.ScriptTarget.ES2022,
module: ts.ModuleKind.CommonJS,
},
}).outputText;
fs.writeFileSync(path.join(directory, `${name}.cjs`), compiled, {
flag: 'wx',
mode: 0o600,
});
manifest[name] = {
source: relative,
sourceSha256: digest(source),
compiledSha256: digest(compiled),
};
}
fs.writeFileSync(
path.join(directory, 'manifest.json'),
JSON.stringify(manifest),
{
flag: 'wx',
mode: 0o600,
},
);
return manifest;
}
function loadPanelClient(directory, port, transport = fetch) {
assert.ok(Number.isSafeInteger(port) && port > 0 && port < 65536);
const origin = `http://127.0.0.1:${port}`;
const manifest = JSON.parse(
fs.readFileSync(path.join(directory, 'manifest.json'), 'utf8'),
);
const requests = [];
let loseCancellation = false;
const context = vm.createContext({
TextDecoder,
Uint8Array,
atob,
crypto: crypto.webcrypto,
fetch: async (relative, options) => {
assert.ok(
typeof relative === 'string' &&
relative.startsWith('/') &&
!relative.startsWith('//'),
);
const url = new URL(relative, origin);
assert.equal(url.origin, origin);
requests.push({
path: relative,
method: options.method,
body: options.body,
});
const response = await transport(url.href, {
...options,
signal: AbortSignal.timeout(10_000),
});
if (
loseCancellation &&
options.method === 'POST' &&
relative.endsWith('/cancellation')
) {
loseCancellation = false;
// Consume a real committed response before simulating transport loss. Never fabricate a receipt.
assert.equal(response.status, 202);
const body = await response.json();
assert.equal(body.status, 'accepted');
throw new Error('Injected response loss after accepted cancellation');
}
return response;
},
});
const load = (name, imports = {}) => {
assert.equal(manifest[name].source, SOURCES[name]);
assert.match(manifest[name].sourceSha256, /^[a-f0-9]{64}$/);
const code = fs.readFileSync(path.join(directory, `${name}.cjs`), 'utf8');
assert.equal(digest(code), manifest[name].compiledSha256);
const mod = { exports: {} };
vm.runInContext(`(function(require,exports,module){${code}\n})`, context)(
(id) => {
assert.ok(Object.hasOwn(imports, id), `Unexpected panel import ${id}`);
return imports[id];
},
mod.exports,
mod,
);
return mod.exports;
};
const auth = load('auth');
const control = load('control', { '@/utils/qinglong3': auth });
return {
auth,
control,
requests,
manifest,
loseNextCancellationResponse() {
loseCancellation = true;
},
};
}
module.exports = { preparePanelClient, loadPanelClient };
@@ -23,13 +23,14 @@ function validateLocalApiCancellationLiveReport(value) {
'artifact',
'compatible',
'observations',
'panelClient',
'platform',
'profile',
'qualification',
'resourceEnvelope',
'schemaVersion',
]);
if (!record || value.schemaVersion !== 1 || !PROFILES.has(value.profile)) {
if (!record || value.schemaVersion !== 2 || !PROFILES.has(value.profile)) {
return Object.freeze({
compatible: false,
findings: ['report identity is invalid'],
@@ -105,6 +106,38 @@ function validateLocalApiCancellationLiveReport(value) {
observed?.sqliteIntegrity !== 'ok'
)
findings.push('API to durable process-stop observations are incomplete');
const panel = value.panelClient;
if (
!exactKeys(panel, [
'authSourceSha256',
'controlSourceSha256',
'unauthenticatedStatus',
'capabilityDiscovered',
'runListed',
'logMarkerObserved',
'restartLogMarkerObserved',
'cancellationResponseLost',
'exactCancellationBodyReplay',
'startPosts',
'cancellationPosts',
'browserRendering',
'ownerProvisioning',
]) ||
!/^[a-f0-9]{64}$/.test(panel?.authSourceSha256 ?? '') ||
!/^[a-f0-9]{64}$/.test(panel?.controlSourceSha256 ?? '') ||
panel?.unauthenticatedStatus !== 401 ||
panel?.capabilityDiscovered !== true ||
panel?.runListed !== true ||
panel?.logMarkerObserved !== true ||
panel?.restartLogMarkerObserved !== true ||
panel?.cancellationResponseLost !== true ||
panel?.exactCancellationBodyReplay !== true ||
panel?.startPosts !== 1 ||
panel?.cancellationPosts !== 2 ||
panel?.browserRendering !== false ||
panel?.ownerProvisioning !== 'seeded_fixture'
)
findings.push('actual panel client execution evidence is incomplete');
if (
!exactKeys(value.qualification, [
'evidenceClass',
@@ -13,6 +13,9 @@ const {
} = require('./ql3-local-api-cancellation-live-audit.cjs');
const ROOT = path.resolve(__dirname, '..');
const {
preparePanelClient,
} = require('./lib/ql3-panel-run-control-live-client.cjs');
const NODE_IMAGE =
'node:24.18.0-bookworm-slim@sha256:6f7b03f7c2c8e2e784dcf9295400527b9b1270fd37b7e9a7285cf83b6951452d';
@@ -80,6 +83,7 @@ function main(argv = process.argv.slice(2)) {
const evidenceRoot = path.join(temporaryRoot, 'evidence');
fs.mkdirSync(evidenceRoot, { mode: 0o700 });
try {
preparePanelClient(ROOT, path.join(evidenceRoot, 'panel-client'));
const artifactOutput = run(process.execPath, [
path.join(ROOT, 'scripts/ql3-local-profile-artifact-audit.cjs'),
`${selected.profile}-application-api`,
@@ -148,7 +152,7 @@ function main(argv = process.argv.slice(2)) {
});
process.stdout.write(
`${JSON.stringify({
schemaVersion: 1,
schemaVersion: 2,
profile: selected.profile,
reportWritten: true,
compatible: true,