Skip to content

Commit b893163

Browse files
committed
test: enforce snapshot owner-facts admission
1 parent e44dfb6 commit b893163

5 files changed

Lines changed: 171 additions & 65 deletions

scripts/layering/runtime-command-cutover-extensions.ts

Lines changed: 0 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,6 @@ const APP_STATE_LEGACY_IMPORT_SOURCES = new Set([
2929
'../../platforms/harmonyos/app-lifecycle.ts',
3030
]);
3131
const APP_STATE_LEGACY_CALLS = new Set(['getAndroidAppState', 'getHarmonyAppState']);
32-
const SNAPSHOT_RUNTIME_BINDING_FILE = 'src/daemon/snapshot-runtime-binding.ts';
33-
const SNAPSHOT_PLATFORM_POLICY_IDENTIFIERS = new Set([
34-
'isIosFamily',
35-
'isIosSimulator',
36-
'providerOwned',
37-
]);
3832

3933
/**
4034
* `devices` proves its single inventory route by binding identity, not by name: the
@@ -195,39 +189,6 @@ export function appStateLegacySessionHandlerViolations(
195189
return violations;
196190
}
197191

198-
/** Snapshot admission consumes owner facts; it must not reconstruct platform/provider policy. */
199-
export function snapshotPlatformPolicyBranchViolations(
200-
sources: ReadonlyMap<string, string>,
201-
): UnruledViolation[] {
202-
const source = sources.get(SNAPSHOT_RUNTIME_BINDING_FILE);
203-
if (source === undefined) {
204-
return [
205-
{
206-
file: SNAPSHOT_RUNTIME_BINDING_FILE,
207-
line: 1,
208-
message: 'snapshot facts-first admission module is missing',
209-
},
210-
];
211-
}
212-
const violations: UnruledViolation[] = [];
213-
const seen = new Set<string>();
214-
const program = parseSync(SNAPSHOT_RUNTIME_BINDING_FILE, source).program as AstNode;
215-
visitAst(program, (node) => {
216-
if (node['type'] !== 'Identifier') return;
217-
const name = identifierName(node);
218-
if (name === undefined || !SNAPSHOT_PLATFORM_POLICY_IDENTIFIERS.has(name) || seen.has(name)) {
219-
return;
220-
}
221-
seen.add(name);
222-
violations.push({
223-
file: SNAPSHOT_RUNTIME_BINDING_FILE,
224-
line: lineOf(source, node),
225-
message: `snapshot admission reconstructs owner policy through ${name}`,
226-
});
227-
});
228-
return violations;
229-
}
230-
231192
function productionSources(sources: ReadonlyMap<string, string>): ProductionSource[] {
232193
return [...sources].map(([path, source]) => ({ path, source }));
233194
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import assert from 'node:assert/strict';
2+
import { test } from 'node:test';
3+
import { snapshotPlatformPolicyBranchViolations } from './runtime-command-cutover-snapshot.ts';
4+
5+
const SNAPSHOT_RUNTIME_BINDING_FILE = 'src/daemon/snapshot-runtime-binding.ts';
6+
const SNAPSHOT_FACTS_FIRST_ADMISSION = `
7+
const facts = await requireRuntimeFacts(params.inspectFacts)(device);
8+
const plan = resolveSnapshotRuntimePlan({
9+
customActions: params.req.flags?.snapshotCustomActions === true,
10+
hasActiveApp: session?.appBundleId !== undefined,
11+
});
12+
for (const operation of plan.use.required) {
13+
const fact = facts.operations[operation];
14+
if (fact.available) continue;
15+
}
16+
`;
17+
18+
function violationsFor(extraAdmission = ''): string[] {
19+
return snapshotPlatformPolicyBranchViolations(
20+
new Map([
21+
[
22+
SNAPSHOT_RUNTIME_BINDING_FILE,
23+
`
24+
function inspectSnapshotCaptureAdmission(params, device, session) {
25+
${SNAPSHOT_FACTS_FIRST_ADMISSION}
26+
${extraAdmission}
27+
}
28+
`,
29+
],
30+
]),
31+
).map(({ file, message }) => `${file}: ${message}`);
32+
}
33+
34+
test('R32 snapshot accepts only the normalized plan and selected operation facts seam', () => {
35+
assert.deepEqual(violationsFor(), []);
36+
});
37+
38+
test('R32 snapshot rejects a direct device-leaf branch in daemon admission', () => {
39+
assert.deepEqual(
40+
violationsFor("if (device.platform === 'apple' && device.kind === 'simulator') return plan;"),
41+
[
42+
'src/daemon/snapshot-runtime-binding.ts: snapshot admission reads device-owner identity instead of selected operation facts',
43+
],
44+
);
45+
});
46+
47+
test('R32 snapshot rejects a provider-mode branch in daemon admission', () => {
48+
assert.deepEqual(
49+
violationsFor("if (facts.device.providerMode === 'provider-runtime') return plan;"),
50+
[
51+
'src/daemon/snapshot-runtime-binding.ts: snapshot admission reads device-owner identity instead of selected operation facts',
52+
],
53+
);
54+
});
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { parseSync } from 'oxc-parser';
2+
import { propertyName, visitAst } from './cutover-policy-ast.ts';
3+
import { countNamedCalls, lineOf, namedFunction } from './runtime-command-cutover-ast.ts';
4+
import type { UnruledViolation } from './runtime-command-cutover-model.ts';
5+
6+
type AstNode = Record<string, unknown>;
7+
8+
const SNAPSHOT_RUNTIME_BINDING_FILE = 'src/daemon/snapshot-runtime-binding.ts';
9+
const SNAPSHOT_ADMISSION_FUNCTION = 'inspectSnapshotCaptureAdmission';
10+
11+
/** Snapshot admission consumes the selected plan and operation facts, never device-owner identity. */
12+
export function snapshotPlatformPolicyBranchViolations(
13+
sources: ReadonlyMap<string, string>,
14+
): UnruledViolation[] {
15+
const source = sources.get(SNAPSHOT_RUNTIME_BINDING_FILE);
16+
if (source === undefined) {
17+
return [
18+
{
19+
file: SNAPSHOT_RUNTIME_BINDING_FILE,
20+
line: 1,
21+
message: 'snapshot facts-first admission module is missing',
22+
},
23+
];
24+
}
25+
const program = parseSync(SNAPSHOT_RUNTIME_BINDING_FILE, source).program as AstNode;
26+
const admission = namedFunction(program, SNAPSHOT_ADMISSION_FUNCTION);
27+
if (admission === undefined) {
28+
return [
29+
{
30+
file: SNAPSHOT_RUNTIME_BINDING_FILE,
31+
line: 1,
32+
message: `snapshot admission must be owned by ${SNAPSHOT_ADMISSION_FUNCTION}`,
33+
},
34+
];
35+
}
36+
37+
const violations: UnruledViolation[] = [];
38+
const seenLines = new Set<number>();
39+
let readsRequiredOperations = false;
40+
let readsOperationFacts = false;
41+
let admitsAvailableFacts = false;
42+
visitAst(admission, (node) => {
43+
const path = memberPath(node);
44+
if (path === undefined) return;
45+
if (samePath(path, ['plan', 'use', 'required'])) readsRequiredOperations = true;
46+
if (samePath(path, ['facts', 'operations'])) readsOperationFacts = true;
47+
if (samePath(path, ['fact', 'available'])) admitsAvailableFacts = true;
48+
49+
const readsDeviceLeaf =
50+
(path[0] === 'device' && path.length > 1) ||
51+
(path[0] === 'params' && path[1] === 'device' && path.length > 2);
52+
const readsOwnerIdentity = path[0] === 'facts' && path[1] === 'device' && path.length > 2;
53+
if (!readsDeviceLeaf && !readsOwnerIdentity) return;
54+
const line = lineOf(source, node);
55+
if (seenLines.has(line)) return;
56+
seenLines.add(line);
57+
violations.push({
58+
file: SNAPSHOT_RUNTIME_BINDING_FILE,
59+
line,
60+
message: 'snapshot admission reads device-owner identity instead of selected operation facts',
61+
});
62+
});
63+
64+
if (countNamedCalls(admission, 'resolveSnapshotRuntimePlan') !== 1) {
65+
violations.push({
66+
file: SNAPSHOT_RUNTIME_BINDING_FILE,
67+
line: lineOf(source, admission),
68+
message: 'snapshot admission must select exactly one normalized runtime plan',
69+
});
70+
}
71+
if (!readsRequiredOperations || !readsOperationFacts || !admitsAvailableFacts) {
72+
violations.push({
73+
file: SNAPSHOT_RUNTIME_BINDING_FILE,
74+
line: lineOf(source, admission),
75+
message: 'snapshot admission must admit every selected operation through owner facts',
76+
});
77+
}
78+
return violations;
79+
}
80+
81+
function memberPath(node: unknown): string[] | undefined {
82+
if (node === null || typeof node !== 'object') return undefined;
83+
const record = node as AstNode;
84+
if (record['type'] === 'Identifier') {
85+
const name = record['name'];
86+
return typeof name === 'string' ? [name] : undefined;
87+
}
88+
if (record['type'] === 'ChainExpression') return memberPath(record['expression']);
89+
if (record['type'] !== 'MemberExpression' || record['computed'] === true) return undefined;
90+
const object = memberPath(record['object']);
91+
const name = propertyName(record['property']);
92+
return object && name ? [...object, name] : undefined;
93+
}
94+
95+
function samePath(actual: readonly string[], expected: readonly string[]): boolean {
96+
return (
97+
actual.length === expected.length && actual.every((part, index) => part === expected[index])
98+
);
99+
}

scripts/layering/runtime-command-cutover-table.test.ts

Lines changed: 17 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,18 @@ import { rowFor, sources, summariesFor } from './runtime-command-cutover-fixture
55
import { cutoverTableDefects } from './runtime-command-cutover-model.ts';
66
import { MIGRATED_COMMAND_CUTOVERS } from './runtime-command-cutover-table.ts';
77
import { commandDescriptors } from '../../src/core/command-descriptor/registry.ts';
8-
import { snapshotPlatformPolicyBranchViolations } from './runtime-command-cutover-extensions.ts';
8+
9+
const SNAPSHOT_FACTS_FIRST_ADMISSION = `
10+
const facts = await requireRuntimeFacts(params.inspectFacts)(device);
11+
const plan = resolveSnapshotRuntimePlan({
12+
customActions: params.req.flags?.snapshotCustomActions === true,
13+
hasActiveApp: session?.appBundleId !== undefined,
14+
});
15+
for (const operation of plan.use.required) {
16+
const fact = facts.operations[operation];
17+
if (fact.available) continue;
18+
}
19+
`;
920

1021
test('R20 boot rejects the superseded root readiness adapter', () => {
1122
assert.deepEqual(
@@ -80,7 +91,11 @@ test('R32 snapshot rejects legacy admission and dispatcher projection', () => {
8091
],
8192
[
8293
'src/daemon/snapshot-runtime-binding.ts',
83-
'export function inspectSnapshotCaptureAdmission() {}',
94+
`
95+
export async function inspectSnapshotCaptureAdmission(params, device, session) {
96+
${SNAPSHOT_FACTS_FIRST_ADMISSION}
97+
}
98+
`,
8499
],
85100
['src/core/dispatch.ts', 'function handleSnapshotCommand() { return legacy.snapshot(); }'],
86101
[
@@ -102,29 +117,6 @@ test('R32 snapshot rejects legacy admission and dispatcher projection', () => {
102117
);
103118
});
104119

105-
test('R32 snapshot rejects platform and provider policy reconstructed in daemon admission', () => {
106-
assert.deepEqual(
107-
snapshotPlatformPolicyBranchViolations(
108-
new Map([
109-
[
110-
'src/daemon/snapshot-runtime-binding.ts',
111-
`
112-
import { isIosFamily, isIosSimulator } from '@agent-device/kernel/device';
113-
function inspectSnapshotCaptureAdmission(device, providerOwned) {
114-
return isIosFamily(device) && isIosSimulator(device) && providerOwned;
115-
}
116-
`,
117-
],
118-
]),
119-
).map(({ file, message }) => `${file}: ${message}`),
120-
[
121-
'src/daemon/snapshot-runtime-binding.ts: snapshot admission reconstructs owner policy through isIosFamily',
122-
'src/daemon/snapshot-runtime-binding.ts: snapshot admission reconstructs owner policy through isIosSimulator',
123-
'src/daemon/snapshot-runtime-binding.ts: snapshot admission reconstructs owner policy through providerOwned',
124-
],
125-
);
126-
});
127-
128120
// Per-row acceptance for the five migrated commands. Every older-row case here was carried over
129121
// from the per-command policy tests these rows replaced; the mechanism itself is proven
130122
// in runtime-command-cutover-policy.test.ts.

scripts/layering/runtime-command-cutover-table.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ import {
88
openLifecycleRouteBindingViolations,
99
prepareLifecycleRouteBindingViolations,
1010
runtimeLifecycleRouteBindingViolations,
11-
snapshotPlatformPolicyBranchViolations,
1211
sourceExecutedUsingDeclarationViolations,
1312
} from './runtime-command-cutover-extensions.ts';
13+
import { snapshotPlatformPolicyBranchViolations } from './runtime-command-cutover-snapshot.ts';
1414
import { recordRuntimeDaemonMechanicsViolations } from './record-runtime-mechanics-policy.ts';
1515
import { retiredDispatchProjectionViolations } from './runtime-command-cutover-descriptor.ts';
1616

0 commit comments

Comments
 (0)