@antprofuse/saddle-islands-tools 0.0.0-stage → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/package.json +15 -4
  2. package/tools/adapters/islands/trigger_source.py +53 -0
  3. package/tools/cli/backend.mjs +24 -0
  4. package/tools/cli/main.py +38 -0
  5. package/tools/cli/minifish.mjs +38 -0
  6. package/tools/cli/minifish.py +52 -0
  7. package/tools/core/package-identity.mjs +23 -0
  8. package/tools/core/package_identity.py +58 -0
  9. package/tools/core/semantic_identity.py +149 -0
  10. package/tools/core/test_semantic_identity.py +136 -0
  11. package/tools/docs/README.md +43 -0
  12. package/tools/docs/backend-testing.md +36 -0
  13. package/tools/docs/iteration.md +34 -0
  14. package/tools/docs/minifish-testing.md +32 -0
  15. package/tools/docs/semantic-identity.md +61 -0
  16. package/tools/modules/backend/actions.mjs +10 -0
  17. package/tools/modules/backend/assert.mjs +20 -0
  18. package/tools/modules/backend/context-receiver.py +30 -0
  19. package/tools/modules/backend/facility.test.mjs +44 -0
  20. package/tools/modules/backend/grouped_context.py +100 -0
  21. package/tools/modules/backend/grpc.mjs +57 -0
  22. package/tools/modules/backend/index.mjs +9 -0
  23. package/tools/modules/backend/input-tree.mjs +14 -0
  24. package/tools/modules/backend/logs.mjs +24 -0
  25. package/tools/modules/backend/logs.test.mjs +28 -0
  26. package/tools/modules/backend/metadata.mjs +66 -0
  27. package/tools/modules/backend/package-lock.json +362 -0
  28. package/tools/modules/backend/package.json +15 -0
  29. package/tools/modules/backend/paths.mjs +11 -0
  30. package/tools/modules/backend/product.mjs +48 -0
  31. package/tools/modules/backend/rejection-logs.mjs +33 -0
  32. package/tools/modules/backend/report.mjs +13 -0
  33. package/tools/modules/backend/report.test.mjs +13 -0
  34. package/tools/modules/backend/run.mjs +50 -0
  35. package/tools/modules/backend/util.mjs +24 -0
  36. package/tools/modules/minifish/checks/guard.test.mjs +13 -0
  37. package/tools/modules/minifish/checks/package.test.mjs +23 -0
  38. package/tools/modules/minifish/compiler/build-case.mjs +39 -0
  39. package/tools/modules/minifish/compiler/case-plan.mjs +175 -0
  40. package/tools/modules/minifish/compiler/config.mjs +29 -0
  41. package/tools/modules/minifish/compiler/observation-module-name.mjs +2 -0
  42. package/tools/modules/minifish/compiler/package-runner.mjs +43 -0
  43. package/tools/modules/minifish/compiler/semantic-module-gate.mjs +27 -0
  44. package/tools/modules/minifish/compiler/tool-identity.mjs +14 -0
  45. package/tools/modules/minifish/harness/batch_cases.py +91 -0
  46. package/tools/modules/minifish/harness/batch_plugin_groups.py +56 -0
  47. package/tools/modules/minifish/harness/batch_report.py +104 -0
  48. package/tools/modules/minifish/harness/browser.mjs +146 -0
  49. package/tools/modules/minifish/harness/build-plugin-artifact.mjs +25 -0
  50. package/tools/modules/minifish/harness/capture_contract.py +78 -0
  51. package/tools/modules/minifish/harness/carousel-observation.mjs +62 -0
  52. package/tools/modules/minifish/harness/carriers.mjs +49 -0
  53. package/tools/modules/minifish/harness/check-independent-batch.py +84 -0
  54. package/tools/modules/minifish/harness/check-runner-isolation.py +102 -0
  55. package/tools/modules/minifish/harness/component_runtime.py +125 -0
  56. package/tools/modules/minifish/harness/contract-value.mjs +32 -0
  57. package/tools/modules/minifish/harness/deferred-visual.mjs +21 -0
  58. package/tools/modules/minifish/harness/device-prepare.mjs +26 -0
  59. package/tools/modules/minifish/harness/execution-evidence.mjs +37 -0
  60. package/tools/modules/minifish/harness/foreground.mjs +20 -0
  61. package/tools/modules/minifish/harness/header-layout.mjs +47 -0
  62. package/tools/modules/minifish/harness/held-snapshot.mjs +67 -0
  63. package/tools/modules/minifish/harness/host-device-fixture.mjs +28 -0
  64. package/tools/modules/minifish/harness/host-language.mjs +73 -0
  65. package/tools/modules/minifish/harness/host-native-ui.mjs +39 -0
  66. package/tools/modules/minifish/harness/host-navigation.mjs +96 -0
  67. package/tools/modules/minifish/harness/image-loading.mjs +35 -0
  68. package/tools/modules/minifish/harness/independent-case-runner.mjs +175 -0
  69. package/tools/modules/minifish/harness/independent-visual.mjs +107 -0
  70. package/tools/modules/minifish/harness/inline-svg.mjs +32 -0
  71. package/tools/modules/minifish/harness/input-lifecycle-evidence.mjs +16 -0
  72. package/tools/modules/minifish/harness/jsapi.mjs +92 -0
  73. package/tools/modules/minifish/harness/loading-input.mjs +52 -0
  74. package/tools/modules/minifish/harness/loading-observation.mjs +25 -0
  75. package/tools/modules/minifish/harness/local-navigation-evidence.mjs +82 -0
  76. package/tools/modules/minifish/harness/miniapp-screenshot.mjs +38 -0
  77. package/tools/modules/minifish/harness/native-dom-sampling.mjs +40 -0
  78. package/tools/modules/minifish/harness/native-enabled.mjs +11 -0
  79. package/tools/modules/minifish/harness/native-rpc-failure.mjs +11 -0
  80. package/tools/modules/minifish/harness/native-state.mjs +14 -0
  81. package/tools/modules/minifish/harness/native-ui-contract.mjs +29 -0
  82. package/tools/modules/minifish/harness/navigation.mjs +19 -0
  83. package/tools/modules/minifish/harness/network_isolation.py +134 -0
  84. package/tools/modules/minifish/harness/painted-source-image.mjs +54 -0
  85. package/tools/modules/minifish/harness/playwright.mjs +1 -0
  86. package/tools/modules/minifish/harness/plugin-events.mjs +16 -0
  87. package/tools/modules/minifish/harness/plugin-package.mjs +23 -0
  88. package/tools/modules/minifish/harness/plugin_artifact.py +23 -0
  89. package/tools/modules/minifish/harness/product_identity.py +8 -0
  90. package/tools/modules/minifish/harness/project_config.py +14 -0
  91. package/tools/modules/minifish/harness/prototype-visual.mjs +123 -0
  92. package/tools/modules/minifish/harness/region-order.mjs +16 -0
  93. package/tools/modules/minifish/harness/renderer-image-evidence.mjs +32 -0
  94. package/tools/modules/minifish/harness/request-guard.mjs +65 -0
  95. package/tools/modules/minifish/harness/request-plan.mjs +124 -0
  96. package/tools/modules/minifish/harness/response-consumption.mjs +106 -0
  97. package/tools/modules/minifish/harness/retained-page-lifecycle.mjs +48 -0
  98. package/tools/modules/minifish/harness/rpc-wire.mjs +34 -0
  99. package/tools/modules/minifish/harness/run-batch.py +133 -0
  100. package/tools/modules/minifish/harness/run-platform.py +293 -0
  101. package/tools/modules/minifish/harness/runtime_isolation.py +71 -0
  102. package/tools/modules/minifish/harness/scroll-container-observation.mjs +40 -0
  103. package/tools/modules/minifish/harness/semantic-anchor.mjs +139 -0
  104. package/tools/modules/minifish/harness/semantic-observation.mjs +197 -0
  105. package/tools/modules/minifish/harness/skeleton-appearance.mjs +49 -0
  106. package/tools/modules/minifish/harness/target-actions.mjs +120 -0
  107. package/tools/modules/minifish/harness/test_batch_concurrency.py +125 -0
  108. package/tools/modules/minifish/harness/test_batch_plugin_groups.py +97 -0
  109. package/tools/modules/minifish/harness/test_batch_runner.py +107 -0
  110. package/tools/modules/minifish/harness/test_common_report.py +47 -0
  111. package/tools/modules/minifish/harness/test_held_capture_contract.py +50 -0
  112. package/tools/modules/minifish/harness/test_pty_tail.py +39 -0
  113. package/tools/modules/minifish/harness/test_runtime_dependencies.py +49 -0
  114. package/tools/modules/minifish/harness/zero-business-plan.mjs +19 -0
  115. package/tools/modules/minifish/identity.py +6 -0
  116. package/tools/modules/minifish/package-lock.json +74 -0
  117. package/tools/modules/minifish/package.json +10 -0
  118. package/tools/modules/minifish/sdk/package-lock.json +57745 -0
  119. package/tools/modules/minifish/sdk/package.json +13 -0
  120. package/tools/modules/workflow/diff_review.py +74 -0
  121. package/tools/modules/workflow/iteration.py +175 -0
  122. package/README.md +0 -3
@@ -0,0 +1,97 @@
1
+ import json
2
+ import runpy
3
+ import sys
4
+ import tempfile
5
+ import unittest
6
+ from pathlib import Path
7
+ from unittest.mock import patch
8
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
9
+ from batch_plugin_groups import plugin_groups as configured_groups
10
+ def plugin_groups(*args, **options):
11
+ return configured_groups(*args, **options, source=Path('/alternate-fixture-source'))
12
+
13
+ class PluginBatchIsolation(unittest.TestCase):
14
+ def setUp(self):
15
+ self.tmp = tempfile.TemporaryDirectory()
16
+ self.addCleanup(self.tmp.cleanup)
17
+ self.root = Path(self.tmp.name)
18
+ self.events = {'instances': [{'id': 'balance', 'events': []}]}
19
+
20
+ def case(self, ident, plugin=False):
21
+ package=self.root/ident; package.mkdir()
22
+ plan={'case': {'id': ident}}
23
+ if plugin: plan['plugin']={'events': self.events}
24
+ (package/'case.spec.mjs').write_text('// Generated\nexport const plan = '+json.dumps(plan)+';\ntest(plan.case.id, async () => {});\n')
25
+ return {'id':ident,'package':str(package)}
26
+
27
+ def mapping(self, mapping):
28
+ file=self.root/'map.json'; file.write_text(json.dumps(mapping)); return file
29
+
30
+ def test_every_case_exactly_once_and_no_cross_plan_shard(self):
31
+ rows=[self.case(str(i),i<9) for i in range(19)]
32
+ mapping={str(i):f'/artifacts/plan-{i%3}' for i in range(9)}
33
+ with patch('batch_plugin_groups.validate_plugin_artifact',return_value={'plan': self.events}) as validate:
34
+ groups=plugin_groups(rows,4,self.mapping(mapping))
35
+ self.assertEqual(validate.call_count,3)
36
+ flattened=[r['id'] for group,artifact in groups for r in group]
37
+ self.assertCountEqual(flattened,[r['id'] for r in rows])
38
+ self.assertEqual(len(flattened),len(set(flattened)))
39
+ for group,artifact in groups:
40
+ self.assertTrue(all(mapping.get(r['id'])==artifact for r in group))
41
+
42
+ def test_unknown_identity_and_invalid_artifact(self):
43
+ rows=[self.case('current',True)]
44
+ with self.assertRaises(ValueError):plugin_groups(rows,4,self.mapping({'stale':'/artifact'}))
45
+ with patch('batch_plugin_groups.validate_plugin_artifact',side_effect=ValueError('changed artifact SHA')):
46
+ with self.assertRaisesRegex(ValueError,'changed artifact SHA'):
47
+ plugin_groups(rows,4,self.mapping({'current':'/artifact'}))
48
+
49
+ def test_default_artifact_is_validated_and_retains_four_shards(self):
50
+ rows=[self.case(str(i),True) for i in range(12)]
51
+ with patch('batch_plugin_groups.validate_plugin_artifact',return_value={'plan': self.events}) as validate:
52
+ groups=plugin_groups(rows,4,default_artifact=Path('/existing'))
53
+ self.assertEqual(validate.call_count,1)
54
+ self.assertEqual(len(groups),4)
55
+ self.assertTrue(all(artifact=='/existing' for _,artifact in groups))
56
+
57
+ def test_all_missing_cases_reported_together(self):
58
+ rows=[self.case('first',True),self.case('ordinary'),self.case('second',True)]
59
+ with self.assertRaises(ValueError) as caught:plugin_groups(rows,4)
60
+ self.assertIn('first: Explicit',str(caught.exception))
61
+ self.assertIn('second: Explicit',str(caught.exception))
62
+ self.assertNotIn('ordinary:',str(caught.exception))
63
+
64
+ def test_wrong_plan_rejected_even_if_artifact_hashes_valid(self):
65
+ rows=[self.case('current',True)]
66
+ with patch('batch_plugin_groups.validate_plugin_artifact',return_value={'plan': {'instances':[{'id':'different'}]}}):
67
+ with self.assertRaisesRegex(ValueError,'plan must match'):
68
+ plugin_groups(rows,2,default_artifact='/artifact')
69
+
70
+ def test_no_plugin_needs_no_artifact(self):
71
+ row=self.case('ordinary')
72
+ self.assertEqual(plugin_groups([row],4),[([row],None)])
73
+
74
+ def test_unreadable_or_changed_plan_not_silently_treated_as_no_plugin(self):
75
+ row=self.case('changed')
76
+ (Path(row['package'])/'case.spec.mjs').write_text('throw new Error("must not execute");')
77
+ with self.assertRaisesRegex(ValueError,'Rebuild case package'):plugin_groups([row],4)
78
+
79
+ def test_real_batch_entry_stops_before_worker_and_report_creation(self):
80
+ rows=[self.case('missing',True)]
81
+ package=Path(rows[0]['package'])
82
+ for name in ['playwright.config.mjs','package.json','package-lock.json','node_modules/.bin/playwright']:
83
+ path=package/name;path.parent.mkdir(parents=True,exist_ok=True);path.write_text('{}')
84
+ manifest=self.root/'manifest.json';manifest.write_text(json.dumps(rows))
85
+ product=self.root/'product';product.mkdir()
86
+ modules=self.root/'modules';(modules/'.bin').mkdir(parents=True);(modules/'.bin/minifish2').touch()
87
+ report=self.root/'report'
88
+ source=self.root/'carrier';source.mkdir()
89
+ config=self.root/'config.json';config.write_text(json.dumps({'schemaVersion':1,'repositoryRoot':'product','pageRoute':'pages/alternate/index','pluginSource':'carrier','localComponentPackages':[]}))
90
+ argv=['run-batch.py','--project-root',str(self.root),'--config',str(config),'--manifest',str(manifest),'--product',str(product),'--node-modules',str(modules),'--report-dir',str(report)]
91
+ with patch.object(sys,'argv',argv),patch('concurrent.futures.ThreadPoolExecutor') as executor,patch('subprocess.Popen') as process:
92
+ with self.assertRaisesRegex(ValueError,'missing: Explicit compiled plugin artifact required'):
93
+ runpy.run_path(str(Path(__file__).resolve().with_name('run-batch.py')),run_name='__main__')
94
+ executor.assert_not_called();process.assert_not_called()
95
+ self.assertFalse(report.exists())
96
+
97
+ if __name__=='__main__':unittest.main()
@@ -0,0 +1,107 @@
1
+ """Infrastructure controls; these do not count as business coverage."""
2
+ import importlib.util
3
+ import json
4
+ from pathlib import Path
5
+ import signal
6
+ import tempfile
7
+ import unittest
8
+ from batch_report import render_report
9
+ from batch_cases import worker_certified, fingerprint, product_fingerprint, read_cases
10
+
11
+ spec = importlib.util.spec_from_file_location('checker', Path(__file__).with_name('check-independent-batch.py'))
12
+ checker = importlib.util.module_from_spec(spec)
13
+ spec.loader.exec_module(checker)
14
+
15
+
16
+ class BatchControls(unittest.TestCase):
17
+ def test_receipt_failures_and_timeout_do_not_skip_next_case(self):
18
+ old = {sig: signal.getsignal(sig) for sig in (signal.SIGINT, signal.SIGTERM)}
19
+ try:
20
+ with tempfile.TemporaryDirectory() as tmp:
21
+ root = Path(tmp)
22
+ rows = []
23
+ for i, mode in enumerate(('pass', 'bad-json', 'wrong-id', 'timeout', 'pass')):
24
+ package = root / str(i)
25
+ binary = package / 'node_modules/.bin/playwright'
26
+ binary.parent.mkdir(parents=True)
27
+ receipt = dict(caseId=str(i), preparation='passed', target='passed', boundary='passed', cleanup='passed', errors=[])
28
+ if mode == 'wrong-id': receipt['caseId'] = 'wrong'
29
+ script = '#!/usr/bin/env python3\nimport os,json,time\nfrom pathlib import Path\n'
30
+ if mode == 'timeout': script += 'time.sleep(30)\n'
31
+ script += "p=Path(os.environ['MINIFISH_TEST_REPORT_DIR']);p.mkdir(parents=True)\n"
32
+ script += f"(p/'case-result.json').write_text({('{' if mode == 'bad-json' else json.dumps(receipt))!r})\n"
33
+ binary.write_text(script); binary.chmod(0o755)
34
+ rows.append(dict(id=str(i), package=str(package)))
35
+ out = root / 'results'; out.mkdir()
36
+ self.assertEqual(checker.execute(rows, out, 1), 1)
37
+ results = json.loads((out / 'batch-results.json').read_text())
38
+ self.assertEqual([r['status'] for r in results], ['passed', 'failed', 'failed', 'failed', 'passed'])
39
+ self.assertEqual(results[3]['error'], 'case timeout')
40
+ finally:
41
+ for sig, handler in old.items(): signal.signal(sig, handler)
42
+
43
+ def test_cleanup_failure_not_excused_by_business_failure(self):
44
+ rows = [dict(id='a', status='passed'), dict(id='b', status='failed')]
45
+ lifecycle = dict(inputsVerified=True, children=[dict(returncode=0)])
46
+ network = dict(namespaceCleanup=dict(remaining=[]), children=[dict(returncode=1)])
47
+ self.assertTrue(worker_certified(rows, lifecycle, network, ['a', 'b'], 1))
48
+ network['namespaceCleanup']['remaining'] = [123]
49
+ self.assertFalse(worker_certified(rows, lifecycle, network, ['a', 'b'], 1))
50
+ network['namespaceCleanup']['remaining'] = []
51
+ self.assertFalse(worker_certified(rows, lifecycle, network, ['a', 'c'], 1))
52
+ lifecycle['inputsVerified'] = False
53
+ self.assertFalse(worker_certified(rows, lifecycle, network, ['a', 'b'], 1))
54
+
55
+ def test_product_copy_fingerprint_excludes_runtime_and_detects_source(self):
56
+ with tempfile.TemporaryDirectory() as tmp:
57
+ root = Path(tmp)
58
+ (root / 'src').mkdir()
59
+ source = root / 'src/app.ts'; source.write_text('original')
60
+ before = product_fingerprint(root)
61
+ (root / 'node_modules').mkdir()
62
+ (root / 'node_modules/cache').write_text('runtime')
63
+ self.assertEqual(before, product_fingerprint(root))
64
+ source.write_text('changed')
65
+ self.assertNotEqual(before, product_fingerprint(root))
66
+
67
+ def test_duplicate_ids_and_packages_rejected_before_launch(self):
68
+ with tempfile.TemporaryDirectory() as tmp:
69
+ root = Path(tmp)
70
+ for name in ('case.spec.mjs', 'playwright.config.mjs', 'package.json', 'package-lock.json', 'node_modules/.bin/playwright'):
71
+ file = root / 'pkg' / name
72
+ file.parent.mkdir(parents=True, exist_ok=True)
73
+ file.write_text('{}')
74
+ manifest = root / 'input.json'
75
+ for rows in ([dict(id='a', package='pkg'), dict(id='b', package='pkg')],
76
+ [dict(id='a', package='pkg'), dict(id='a', package='other')],
77
+ [dict(id='a', package=None)]):
78
+ manifest.write_text(json.dumps(rows))
79
+ with self.assertRaises(ValueError): read_cases(manifest)
80
+
81
+ def test_html_escapes_errors_and_rejects_outside_image_links(self):
82
+ with tempfile.TemporaryDirectory() as tmp:
83
+ root = Path(tmp); report = root / 'report'; report.mkdir()
84
+ (root / 'private.png').write_bytes(b'not-an-image')
85
+ receipt = report / 'case-result.json'
86
+ receipt.write_text(json.dumps(dict(errors=['<script>alert(1)</script>'], screenshots=[dict(file='../private.png', phase='target')], artifactErrors=[])))
87
+ summary = dict(startedAt=0, finishedAt=1, counts={'failed':1}, cases=[dict(id='区域 / <bad>', status='failed', receipt=str(receipt))])
88
+ output = render_report(summary, report).read_text()
89
+ self.assertNotIn('<script>alert(1)</script>', output)
90
+ self.assertIn('&lt;script&gt;', output)
91
+ self.assertIn('&lt;bad&gt;', output)
92
+ self.assertNotIn('<img ', output)
93
+ self.assertIn('无截图', output)
94
+
95
+ def test_runner_source_mutation_changes_fingerprint(self):
96
+ with tempfile.TemporaryDirectory() as tmp:
97
+ root = Path(tmp)
98
+ for name in ('case.spec.mjs', 'playwright.config.mjs', 'package.json', 'package-lock.json'):
99
+ (root / name).write_text('{}')
100
+ (root / 'runner').mkdir()
101
+ source = root / 'runner/main.mjs'; source.write_text('a')
102
+ rows = [dict(id='a', package=tmp)]
103
+ before = fingerprint(rows)
104
+ source.write_text('b')
105
+ self.assertNotEqual(before, fingerprint(rows))
106
+
107
+ if __name__ == '__main__': unittest.main()
@@ -0,0 +1,47 @@
1
+ import copy
2
+ import json
3
+ from pathlib import Path
4
+ import sys
5
+ import tempfile
6
+ import unittest
7
+
8
+ from batch_report import render_report
9
+ from plugin_artifact import validate_plugin_artifact
10
+
11
+ class CommonReportControls(unittest.TestCase):
12
+ def test_current_only_main_image_and_old_identity_in_history(self):
13
+ with tempfile.TemporaryDirectory() as tmp:
14
+ root=Path(tmp)
15
+ (root/'current.png').write_bytes(b'PNG-current')
16
+ (root/'old.png').write_bytes(b'PNG-old')
17
+ cases=[]
18
+ for name,sha in [('current','new-sha'),('old','old-sha')]:
19
+ receipt=root/(name+'.json');receipt.write_text(json.dumps(dict(caseId=name,executionId=name,productIdentity=dict(sha256=sha),errors=[],artifactErrors=[],screenshots=[dict(file=name+'.png',phase='target')])));cases.append(dict(id=name,status='passed',receipt=str(receipt)))
20
+ html=render_report(dict(currentProduct=dict(sha256='new-sha'),cases=cases,counts=dict(passed=2),startedAt=0,finishedAt=1),root).read_text()
21
+ current=html.split('data-status="passed"')[1];old=html.split('data-status="passed"')[2]
22
+ self.assertIn('data:image/png;base64',current.split('class="shots current-product"')[1].split('</div>')[0])
23
+ self.assertNotIn('<img ',old.split('class="shots current-product"')[1].split('</div>')[0])
24
+ self.assertIn('历史执行截图(非当前产品)',old);self.assertIn('old-sha',old);self.assertIn('当前产品最终图缺失',old)
25
+ def test_missing_current_identity_never_adopts_legacy_screenshot(self):
26
+ with tempfile.TemporaryDirectory() as tmp:
27
+ root=Path(tmp);(root/'old.png').write_bytes(b'PNG');receipt=root/'old.json';receipt.write_text(json.dumps(dict(errors=[],screenshots=[dict(file='old.png',phase='target')],fingerprints=dict(productSha256='legacy-sha'))))
28
+ html=render_report(dict(currentProduct=dict(sha256='new-sha'),cases=[dict(id='old',status='passed',receipt=str(receipt))],startedAt=0,finishedAt=1),root).read_text()
29
+ self.assertNotIn('<img ',html.split('class="shots current-product"')[1].split('</div>')[0]);self.assertIn('legacy-sha',html);self.assertIn('<details class="history">',html)
30
+ def test_artifact_missing_modified_and_source_mismatch_preflight(self):
31
+ import hashlib
32
+ with tempfile.TemporaryDirectory() as tmp:
33
+ source=Path(tmp)/'source';source.mkdir();(source/'carrier.js').write_text('controlled source')
34
+ copydir=Path(tmp)/'plugin';copydir.mkdir()
35
+ for name in ['appConfig.json','index.worker.js','index.js']:(copydir/name).write_text('facility artifact')
36
+ digest=lambda p:hashlib.sha256(p.read_bytes()).hexdigest()
37
+ manifest=dict(schemaVersion=1,plan=dict(instances=[dict(id='facility')]),sourceHashes={'carrier.js':digest(source/'carrier.js')},artifacts={p.name:digest(p) for p in copydir.iterdir()})
38
+ (copydir/'plugin-artifact.json').write_text(json.dumps(manifest))
39
+ validate_plugin_artifact(copydir,source)
40
+ (copydir/'index.worker.js').write_text('changed')
41
+ with self.assertRaises(ValueError):validate_plugin_artifact(copydir,source)
42
+ (copydir/'index.worker.js').unlink()
43
+ with self.assertRaises(ValueError):validate_plugin_artifact(copydir,source)
44
+ with self.assertRaises(FileNotFoundError):validate_plugin_artifact(Path(tmp)/'absent',source)
45
+ manifest=json.loads((copydir/'plugin-artifact.json').read_text());manifest['sourceHashes']={};(copydir/'plugin-artifact.json').write_text(json.dumps(manifest))
46
+ with self.assertRaises(ValueError):validate_plugin_artifact(copydir,source)
47
+ if __name__=='__main__':unittest.main()
@@ -0,0 +1,50 @@
1
+ import copy
2
+ import json
3
+ from pathlib import Path
4
+ import tempfile
5
+ import unittest
6
+ from capture_contract import classify_receipt, associate_capture
7
+ from batch_cases import worker_certified
8
+ from batch_report import render_report
9
+
10
+
11
+ def behavior():
12
+ return dict(caseId='held',executionRole='behavior',executionId='behavior-1',outcome='passed',preparation='passed',target='passed',boundary='passed',cleanup='passed',errors=[],artifactErrors=[],screenshots=[],fingerprints=dict(caseId='held',**{k:'a'*64 for k in ['planSha256','sourceSha256','productSha256','packageSha256','dependencyLockSha256']}))
13
+
14
+
15
+ def artifact():
16
+ a=behavior();a.update(executionRole='held-artifact',executionId='capture-1',outcome='artifact-only',boundary='not-executed-artifact-only',screenshots=[dict(file='held.png',phase='target')],notExecutedActions=[dict(kind='release',boundaryId='open',status='not-executed-artifact-only')],actions=[dict(kind='cancel',status='passed',protocol='native-move-outside-device-then-up')],artifactAudit=dict(status='verified',violations=[],expectations=[dict(id='prepare',status='completed'),dict(id='open',status='not-executed-artifact-only')]),evidence=dict(violations=[],networkViolations=[],expectations=[dict(id='prepare',times=1,received=1,delivered=1,consumed=1,outcome=dict(kind='response'),requireConsumption=True),dict(id='open',times=1,received=0,delivered=0,consumed=0,outcome=dict(kind='response'),requireConsumption=True)]));return a
17
+
18
+
19
+ class HeldCaptureControls(unittest.TestCase):
20
+ def test_artifact_is_never_behavior_pass_and_all_cancellation_audits_required(self):
21
+ a=artifact();self.assertEqual(classify_receipt(a,'held',0,'held-artifact'),'artifact-only');self.assertEqual(classify_receipt(a,'held',0),'failed')
22
+ for mutate in [lambda a:a.update(boundary='passed'),lambda a:a.update(actions=[]),lambda a:a['evidence'].update(networkViolations=['unknown external call']),lambda a:a['evidence']['expectations'][1].update(received=1),lambda a:a['evidence']['expectations'][0].update(consumed=0),lambda a:a['artifactAudit'].update(violations=['unexpected operation']),lambda a:a.update(executionId=None)]:
23
+ bad=copy.deepcopy(a);mutate(bad);self.assertEqual(classify_receipt(bad,'held',0,'held-artifact'),'failed')
24
+ forged=behavior();forged['outcome']='artifact-only';self.assertEqual(classify_receipt(forged,'held',0),'failed')
25
+ b=behavior();b.update(boundary='failed',errors=['missing navigation']);self.assertEqual(classify_receipt(b,'held',0),'failed');associate_capture(b,a)
26
+
27
+ def test_different_inputs_product_source_package_lock_or_execution_identity_reject(self):
28
+ b,a=behavior(),artifact();self.assertEqual(associate_capture(b,a)['captureOutcome'],'artifact-only')
29
+ for key in a['fingerprints']:
30
+ bad=copy.deepcopy(a);bad['fingerprints'][key]='b'*64
31
+ with self.assertRaises(ValueError):associate_capture(b,bad)
32
+ a['executionId']=b['executionId']
33
+ with self.assertRaises(ValueError):associate_capture(b,a)
34
+
35
+ def test_legacy_dependency_mismatch_rejects_and_old_failure_is_preserved(self):
36
+ b,a=behavior(),artifact();b['packageFiles']={'case.spec.mjs':'1','runner/regions/T4.mjs':'2','runner/miniapp-screenshot.mjs':'new'};a['packageFiles']={'case.spec.mjs':'1','runner/regions/T4.mjs':'2','runner/miniapp-screenshot.mjs':'old'};a.update(historicalStatus='failed',outcome='failed');a['fingerprints'].update(sourceSha256='b'*64,packageSha256='c'*64)
37
+ association=associate_capture(b,a,legacy=True);self.assertEqual(association['captureOutcome'],'failed');self.assertEqual(association['changedLifecycleDependencies'],['runner/miniapp-screenshot.mjs']);a['packageFiles']['runner/regions/T4.mjs']='different'
38
+ with self.assertRaises(ValueError):associate_capture(b,a,legacy=True)
39
+
40
+ def test_artifact_worker_cleanup_and_failed_behavior_html_remain_distinct(self):
41
+ lifecycle=dict(inputsVerified=True,children=[dict(returncode=0)]);network=dict(namespaceCleanup=dict(remaining=[]),children=[dict(returncode=0)])
42
+ self.assertTrue(worker_certified([dict(id='held',status='artifact-only')],lifecycle,network,['held'],0));network['namespaceCleanup']['remaining']=[1];self.assertFalse(worker_certified([dict(id='held',status='artifact-only')],lifecycle,network,['held'],0))
43
+ with tempfile.TemporaryDirectory() as tmp:
44
+ p=Path(tmp);b=behavior();b.update(outcome='failed',boundary='failed',errors=['missing navigation']);a=artifact();(p/'behavior.json').write_text(json.dumps(b));(p/'capture.json').write_text(json.dumps(a));(p/'held.png').write_bytes(b'PNG')
45
+ summary=dict(startedAt=0,finishedAt=1,counts=dict(passed=0,failed=1),cases=[dict(id='held',status='failed',receipt=str(p/'behavior.json'),captureEvidence=dict(receipt=str(p/'capture.json')),phases=dict(boundary='failed'))]);html=render_report(summary,p).read_text();self.assertIn('按住态展示截图(独立采集)',html);self.assertIn('behavior-1',html);self.assertIn('capture-1',html);self.assertIn('data-status="failed"',html);self.assertIn('missing navigation',html)
46
+ a['fingerprints']['planSha256']='d'*64;(p/'capture.json').write_text(json.dumps(a))
47
+ with self.assertRaises(ValueError):render_report(summary,p)
48
+
49
+
50
+ if __name__=='__main__':unittest.main()
@@ -0,0 +1,39 @@
1
+ import io
2
+ import os
3
+ import pty
4
+ import sys
5
+ from pathlib import Path
6
+ import threading
7
+ import time
8
+ import unittest
9
+ sys.path.insert(0,str(Path(__file__).resolve().parent))
10
+ from runtime_isolation import drain_pty
11
+
12
+ class TailTests(unittest.TestCase):
13
+ def test_closed_slave_eio_and_large_tail(self):
14
+ master,slave=pty.openpty()
15
+ expected=b'x'*200000
16
+ def write_tail():
17
+ try:
18
+ view=memoryview(expected)
19
+ while view:
20
+ n=os.write(slave,view);view=view[n:]
21
+ finally:os.close(slave)
22
+ writer=threading.Thread(target=write_tail);writer.start()
23
+ try:
24
+ log=io.BytesIO()
25
+ self.assertEqual(drain_pty(master,log,budget=2),len(expected))
26
+ self.assertEqual(log.getvalue(),expected)
27
+ writer.join(timeout=2)
28
+ self.assertFalse(writer.is_alive())
29
+ finally:os.close(master)
30
+
31
+ def test_live_quiet_slave_has_bounded_wait(self):
32
+ master,slave=pty.openpty()
33
+ try:
34
+ begin=time.monotonic()
35
+ self.assertEqual(drain_pty(master,io.BytesIO(),budget=.1),0)
36
+ self.assertLess(time.monotonic()-begin,.5)
37
+ finally:os.close(slave);os.close(master)
38
+
39
+ if __name__=='__main__':unittest.main()
@@ -0,0 +1,49 @@
1
+ """Official component isolation must retain bytes and worker write ownership."""
2
+ import io
3
+ from pathlib import Path
4
+ import tarfile
5
+ import tempfile
6
+ import unittest
7
+ from runtime_isolation import prepare_dependencies
8
+
9
+
10
+ class DependencyLayoutTest(unittest.TestCase):
11
+ def test_component_materialization_preserves_bytes_and_archive(self):
12
+ with tempfile.TemporaryDirectory() as directory:
13
+ root = Path(directory)
14
+ modules = root / 'installed'
15
+ official = modules / '@alipay/industry-mini/es/ErrorBlock'
16
+ official.mkdir(parents=True)
17
+ (official / 'index.json').write_bytes(b'{"component":true}')
18
+ other = modules / '@alipay/tool'
19
+ other.mkdir()
20
+ archive = root / 'views.tgz'
21
+ with tarfile.open(archive, 'w:gz') as output:
22
+ entry = tarfile.TarInfo('package/index.js')
23
+ data = b'original generated component'
24
+ entry.size = len(data)
25
+ output.addfile(entry, io.BytesIO(data))
26
+ destinations = [root / 'worker1/node_modules', root / 'worker2/node_modules']
27
+ for destination in destinations:
28
+ destination.parent.mkdir()
29
+ prepare_dependencies(modules, destination, {'alternate-views': archive})
30
+ component = destination / '@alipay/industry-mini/es/ErrorBlock/index.json'
31
+ self.assertEqual(component.read_bytes(), (official / 'index.json').read_bytes())
32
+ self.assertTrue(component.resolve().is_relative_to(destination.parent))
33
+ self.assertTrue((destination / '@alipay/tool').is_symlink())
34
+ self.assertEqual((destination / 'alternate-views/index.js').read_bytes(), data)
35
+ (destinations[0] / '@alipay/industry-mini/es/ErrorBlock/index.json').write_text('worker mutation')
36
+ self.assertEqual((official / 'index.json').read_bytes(), b'{"component":true}')
37
+ self.assertEqual((destinations[1] / '@alipay/industry-mini/es/ErrorBlock/index.json').read_bytes(), b'{"component":true}')
38
+
39
+ def test_absent_component_keeps_original_scope_link(self):
40
+ with tempfile.TemporaryDirectory() as directory:
41
+ root = Path(directory)
42
+ modules = root / 'installed'
43
+ (modules / '@alipay/tool').mkdir(parents=True)
44
+ prepare_dependencies(modules, root / 'isolated')
45
+ self.assertTrue((root / 'isolated/@alipay').is_symlink())
46
+
47
+
48
+ if __name__ == '__main__':
49
+ unittest.main()
@@ -0,0 +1,19 @@
1
+ import assert from 'node:assert/strict';
2
+
3
+ /** Exact zero-call contract. Never use as a fallback for a nonempty request plan. */
4
+ export function zeroBusinessPlan() {
5
+ const attempts = [], violations = [];
6
+ return {
7
+ async receive(operation, request, wire) {
8
+ attempts.push({ operation, request, wire });
9
+ const message = `Unexpected business call in zero-call case: ${operation}`;
10
+ violations.push(message);
11
+ throw Error(message);
12
+ },
13
+ markDelivered() { violations.push('unplanned response delivered'); },
14
+ markConsumed() { violations.push('unplanned response consumed'); },
15
+ assertComplete() { assert.deepEqual(violations, [], 'zero-call boundary violations'); },
16
+ evidence() { return { attempts: structuredClone(attempts), violations: [...violations], expectations: [] }; },
17
+ close() {},
18
+ };
19
+ }
@@ -0,0 +1,6 @@
1
+ """Compatibility import within the tool; common identity semantics live in core."""
2
+ import sys
3
+ from pathlib import Path
4
+ sys.dont_write_bytecode = True
5
+ sys.path.insert(0, str(Path(__file__).resolve().parents[3]))
6
+ from tools.core.package_identity import verify_package
@@ -0,0 +1,74 @@
1
+ {
2
+ "name": "minifish-test-tools",
3
+ "lockfileVersion": 3,
4
+ "requires": true,
5
+ "packages": {
6
+ "": {
7
+ "name": "minifish-test-tools",
8
+ "version": "1.0.0",
9
+ "dependencies": {
10
+ "@playwright/test": "1.63.0",
11
+ "typescript": "5.9.3"
12
+ }
13
+ },
14
+ "node_modules/@playwright/test": {
15
+ "version": "1.63.0",
16
+ "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.63.0.tgz",
17
+ "integrity": "sha512-oxMK4vllB9RK5NQ2l1pq1IfOf2AvnEuj/vYGDj0H2nMtmtZpKtCwt/l00GEO6xjGfpBNAvjovvYdCm50dRQkpQ==",
18
+ "dev": true,
19
+ "license": "Apache-2.0",
20
+ "dependencies": {
21
+ "playwright": "1.63.0"
22
+ },
23
+ "bin": {
24
+ "playwright": "cli.js"
25
+ },
26
+ "engines": {
27
+ "node": ">=20"
28
+ }
29
+ },
30
+ "node_modules/playwright": {
31
+ "version": "1.63.0",
32
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.63.0.tgz",
33
+ "integrity": "sha512-+7ziBLidS4NaNCdt57SUDT+wYmmd5fmiQejUic/kb+YsYSCPyOOE9sebzMjNmQrsnNpDJqd4WHvV/8lfKfUDUg==",
34
+ "dev": true,
35
+ "license": "Apache-2.0",
36
+ "dependencies": {
37
+ "playwright-core": "1.63.0"
38
+ },
39
+ "bin": {
40
+ "playwright": "cli.js"
41
+ },
42
+ "engines": {
43
+ "node": ">=20"
44
+ }
45
+ },
46
+ "node_modules/playwright-core": {
47
+ "version": "1.63.0",
48
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.63.0.tgz",
49
+ "integrity": "sha512-rYCsBF/M5HjUch52bbtVONEFjv6Xu8sm8h72dNlR5bzIE1fvC/bxgspzkjSfU+MweEMmPM8KJebG6nnyxo5mCg==",
50
+ "dev": true,
51
+ "license": "Apache-2.0",
52
+ "bin": {
53
+ "playwright-core": "cli.js"
54
+ },
55
+ "engines": {
56
+ "node": ">=20"
57
+ }
58
+ },
59
+ "node_modules/typescript": {
60
+ "version": "5.9.3",
61
+ "resolved": "https://registry.antgroup-inc.cn/typescript/-/typescript-5.9.3.tgz",
62
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
63
+ "dev": true,
64
+ "bin": {
65
+ "tsc": "bin/tsc",
66
+ "tsserver": "bin/tsserver"
67
+ },
68
+ "engines": {
69
+ "node": ">=14.17"
70
+ }
71
+ }
72
+ },
73
+ "version": "1.0.0"
74
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "name": "minifish-test-tools",
3
+ "version": "1.0.0",
4
+ "dependencies": {
5
+ "@playwright/test": "1.63.0",
6
+ "typescript": "5.9.3"
7
+ },
8
+ "private": true,
9
+ "type": "module"
10
+ }