@xioflow/kernel 0.3.0 → 0.5.0

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 (165) hide show
  1. package/README.md +174 -15
  2. package/dist/capability/check.d.ts +32 -0
  3. package/dist/capability/check.d.ts.map +1 -0
  4. package/dist/capability/check.js +136 -0
  5. package/dist/capability/check.js.map +1 -0
  6. package/dist/capability/index.d.ts +3 -0
  7. package/dist/capability/index.d.ts.map +1 -0
  8. package/dist/capability/index.js +3 -0
  9. package/dist/capability/index.js.map +1 -0
  10. package/dist/capability/path-utils.d.ts +11 -0
  11. package/dist/capability/path-utils.d.ts.map +1 -0
  12. package/dist/capability/path-utils.js +46 -0
  13. package/dist/capability/path-utils.js.map +1 -0
  14. package/dist/cli.d.ts +3 -0
  15. package/dist/cli.d.ts.map +1 -0
  16. package/dist/cli.js +94 -0
  17. package/dist/cli.js.map +1 -0
  18. package/dist/confinement/bubblewrap.d.ts +8 -0
  19. package/dist/confinement/bubblewrap.d.ts.map +1 -0
  20. package/dist/confinement/bubblewrap.js +45 -0
  21. package/dist/confinement/bubblewrap.js.map +1 -0
  22. package/dist/confinement/detector.d.ts +4 -0
  23. package/dist/confinement/detector.d.ts.map +1 -0
  24. package/dist/confinement/detector.js +35 -0
  25. package/dist/confinement/detector.js.map +1 -0
  26. package/dist/confinement/index.d.ts +5 -0
  27. package/dist/confinement/index.d.ts.map +1 -0
  28. package/dist/confinement/index.js +5 -0
  29. package/dist/confinement/index.js.map +1 -0
  30. package/dist/confinement/sandbox-exec.d.ts +8 -0
  31. package/dist/confinement/sandbox-exec.d.ts.map +1 -0
  32. package/dist/confinement/sandbox-exec.js +30 -0
  33. package/dist/confinement/sandbox-exec.js.map +1 -0
  34. package/dist/confinement/srt.d.ts +8 -0
  35. package/dist/confinement/srt.d.ts.map +1 -0
  36. package/dist/confinement/srt.js +41 -0
  37. package/dist/confinement/srt.js.map +1 -0
  38. package/dist/domain.d.ts +26 -2
  39. package/dist/domain.d.ts.map +1 -1
  40. package/dist/domain.js +192 -18
  41. package/dist/domain.js.map +1 -1
  42. package/dist/driver/node-driver.d.ts.map +1 -1
  43. package/dist/driver/node-driver.js +42 -61
  44. package/dist/driver/node-driver.js.map +1 -1
  45. package/dist/driver/reaper-driver.d.ts +62 -0
  46. package/dist/driver/reaper-driver.d.ts.map +1 -0
  47. package/dist/driver/reaper-driver.js +371 -0
  48. package/dist/driver/reaper-driver.js.map +1 -0
  49. package/dist/driver/spawn-support.d.ts +4 -0
  50. package/dist/driver/spawn-support.d.ts.map +1 -0
  51. package/dist/driver/spawn-support.js +31 -0
  52. package/dist/driver/spawn-support.js.map +1 -0
  53. package/dist/driver/types.d.ts +19 -2
  54. package/dist/driver/types.d.ts.map +1 -1
  55. package/dist/fault/crashpoint.d.ts +8 -0
  56. package/dist/fault/crashpoint.d.ts.map +1 -0
  57. package/dist/fault/crashpoint.js +37 -0
  58. package/dist/fault/crashpoint.js.map +1 -0
  59. package/dist/index.d.ts +9 -0
  60. package/dist/index.d.ts.map +1 -1
  61. package/dist/index.js +6 -0
  62. package/dist/index.js.map +1 -1
  63. package/dist/mcp/server.d.ts +49 -0
  64. package/dist/mcp/server.d.ts.map +1 -0
  65. package/dist/mcp/server.js +233 -0
  66. package/dist/mcp/server.js.map +1 -0
  67. package/dist/mcp/tools.d.ts +32 -0
  68. package/dist/mcp/tools.d.ts.map +1 -0
  69. package/dist/mcp/tools.js +255 -0
  70. package/dist/mcp/tools.js.map +1 -0
  71. package/dist/native/darwin-arm64/xioflow-reaper +0 -0
  72. package/dist/native/darwin-x64/xioflow-reaper +0 -0
  73. package/dist/native/linux-arm64/xioflow-reaper +0 -0
  74. package/dist/native/linux-x64/xioflow-reaper +0 -0
  75. package/dist/otel/otlp.d.ts +37 -0
  76. package/dist/otel/otlp.d.ts.map +1 -0
  77. package/dist/otel/otlp.js +224 -0
  78. package/dist/otel/otlp.js.map +1 -0
  79. package/dist/quick-run.d.ts.map +1 -1
  80. package/dist/quick-run.js +5 -12
  81. package/dist/quick-run.js.map +1 -1
  82. package/dist/recovery/engine.d.ts +7 -0
  83. package/dist/recovery/engine.d.ts.map +1 -1
  84. package/dist/recovery/engine.js +176 -4
  85. package/dist/recovery/engine.js.map +1 -1
  86. package/dist/snapshot/git-shadow.d.ts +57 -0
  87. package/dist/snapshot/git-shadow.d.ts.map +1 -0
  88. package/dist/snapshot/git-shadow.js +364 -0
  89. package/dist/snapshot/git-shadow.js.map +1 -0
  90. package/dist/store/schema.d.ts +1 -1
  91. package/dist/store/schema.d.ts.map +1 -1
  92. package/dist/store/schema.js +31 -0
  93. package/dist/store/schema.js.map +1 -1
  94. package/dist/store/sqlite.d.ts +14 -1
  95. package/dist/store/sqlite.d.ts.map +1 -1
  96. package/dist/store/sqlite.js +150 -58
  97. package/dist/store/sqlite.js.map +1 -1
  98. package/dist/supervisor/admission.d.ts +26 -0
  99. package/dist/supervisor/admission.d.ts.map +1 -0
  100. package/dist/supervisor/admission.js +98 -0
  101. package/dist/supervisor/admission.js.map +1 -0
  102. package/dist/supervisor/drainer.d.ts +17 -0
  103. package/dist/supervisor/drainer.d.ts.map +1 -0
  104. package/dist/supervisor/drainer.js +200 -0
  105. package/dist/supervisor/drainer.js.map +1 -0
  106. package/dist/supervisor/finalize.d.ts +21 -0
  107. package/dist/supervisor/finalize.d.ts.map +1 -0
  108. package/dist/supervisor/finalize.js +66 -0
  109. package/dist/supervisor/finalize.js.map +1 -0
  110. package/dist/supervisor/fingerprint.d.ts +8 -0
  111. package/dist/supervisor/fingerprint.d.ts.map +1 -0
  112. package/dist/supervisor/fingerprint.js +54 -0
  113. package/dist/supervisor/fingerprint.js.map +1 -0
  114. package/dist/supervisor/process-monitor.d.ts +9 -0
  115. package/dist/supervisor/process-monitor.d.ts.map +1 -0
  116. package/dist/supervisor/process-monitor.js +230 -0
  117. package/dist/supervisor/process-monitor.js.map +1 -0
  118. package/dist/supervisor/process-output.d.ts +27 -0
  119. package/dist/supervisor/process-output.d.ts.map +1 -0
  120. package/dist/supervisor/process-output.js +102 -0
  121. package/dist/supervisor/process-output.js.map +1 -0
  122. package/dist/supervisor/process-run.d.ts +30 -0
  123. package/dist/supervisor/process-run.d.ts.map +1 -0
  124. package/dist/supervisor/process-run.js +191 -0
  125. package/dist/supervisor/process-run.js.map +1 -0
  126. package/dist/supervisor/replay.d.ts +14 -0
  127. package/dist/supervisor/replay.d.ts.map +1 -0
  128. package/dist/supervisor/replay.js +70 -0
  129. package/dist/supervisor/replay.js.map +1 -0
  130. package/dist/supervisor/rollback.d.ts +8 -0
  131. package/dist/supervisor/rollback.d.ts.map +1 -0
  132. package/dist/supervisor/rollback.js +153 -0
  133. package/dist/supervisor/rollback.js.map +1 -0
  134. package/dist/supervisor/service.d.ts +24 -0
  135. package/dist/supervisor/service.d.ts.map +1 -0
  136. package/dist/supervisor/service.js +588 -0
  137. package/dist/supervisor/service.js.map +1 -0
  138. package/dist/supervisor/snapshot-ops.d.ts +36 -0
  139. package/dist/supervisor/snapshot-ops.d.ts.map +1 -0
  140. package/dist/supervisor/snapshot-ops.js +243 -0
  141. package/dist/supervisor/snapshot-ops.js.map +1 -0
  142. package/dist/supervisor/supervisor.d.ts +62 -30
  143. package/dist/supervisor/supervisor.d.ts.map +1 -1
  144. package/dist/supervisor/supervisor.js +164 -842
  145. package/dist/supervisor/supervisor.js.map +1 -1
  146. package/dist/supervisor/types.d.ts +64 -0
  147. package/dist/supervisor/types.d.ts.map +1 -0
  148. package/dist/supervisor/types.js +2 -0
  149. package/dist/supervisor/types.js.map +1 -0
  150. package/dist/testing/contract-suite.d.ts.map +1 -1
  151. package/dist/testing/contract-suite.js +619 -12
  152. package/dist/testing/contract-suite.js.map +1 -1
  153. package/dist/types.d.ts +161 -3
  154. package/dist/types.d.ts.map +1 -1
  155. package/dist/types.js +8 -0
  156. package/dist/types.js.map +1 -1
  157. package/dist/workspace/read-tracking.d.ts +23 -0
  158. package/dist/workspace/read-tracking.d.ts.map +1 -0
  159. package/dist/workspace/read-tracking.js +66 -0
  160. package/dist/workspace/read-tracking.js.map +1 -0
  161. package/dist/workspace/transactions.d.ts +89 -0
  162. package/dist/workspace/transactions.d.ts.map +1 -0
  163. package/dist/workspace/transactions.js +277 -0
  164. package/dist/workspace/transactions.js.map +1 -0
  165. package/package.json +8 -2
@@ -2,8 +2,26 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import crypto from 'node:crypto';
5
- import { spawn } from 'node:child_process';
6
- import { ExecutionDomain, SqliteStore, ProcessSupervisor, RecoveryEngine, ResourceConflictError, UnsupportedCapabilityError, EpochFencedError, OperationNotActiveError, DuplicateOperationError, OperationIdConflictError, RecoveryRequiredError, computeInputFingerprint, } from '../index.js';
5
+ import util from 'node:util';
6
+ import { spawn, execFile } from 'node:child_process';
7
+ const execFileAsync = util.promisify(execFile);
8
+ import { ExecutionDomain, SqliteStore, ProcessSupervisor, RecoveryEngine, ResourceConflictError, UnsupportedCapabilityError, EpochFencedError, OperationNotActiveError, DuplicateOperationError, OperationIdConflictError, RecoveryRequiredError, computeInputFingerprint, normalizeResourceName, CapabilityViolationError, } from '../index.js';
9
+ /** 进程是否已真正消失(容许 init / 持有者回收僵尸的短暂延迟)。 */
10
+ async function waitUntilGone(pid, timeoutMs) {
11
+ const deadline = Date.now() + timeoutMs;
12
+ for (;;) {
13
+ try {
14
+ process.kill(pid, 0);
15
+ }
16
+ catch (err) {
17
+ if (err.code === 'ESRCH')
18
+ return true;
19
+ }
20
+ if (Date.now() >= deadline)
21
+ return false;
22
+ await new Promise((r) => setTimeout(r, 25));
23
+ }
24
+ }
7
25
  function ensureTaskAndRun(domain, taskId, runId) {
8
26
  const store = domain.getStore();
9
27
  if (!store.getTask(taskId)) {
@@ -369,7 +387,7 @@ export function defineContractTestSuite(consumerName, createContext) {
369
387
  expect(grandchildAliveAfter).toBe(false);
370
388
  await executePromise;
371
389
  });
372
- it('契约 10: 逃逸后代诚实上报,驱动无法确认停止时如实标记 cannot_determine', async () => {
390
+ it('契约 10: 逃逸后代诚实上报:停止结论与事实一致,无法确认时如实标记 cannot_determine', async () => {
373
391
  const { supervisor, domain, tempDir } = ctx;
374
392
  ensureTaskAndRun(domain, 'task-c10', 'run-c10');
375
393
  const escapedPidFile = path.join(tempDir, 'escaped.pid');
@@ -421,9 +439,19 @@ export function defineContractTestSuite(consumerName, createContext) {
421
439
  }
422
440
  catch { }
423
441
  expect(escapedAliveBefore).toBe(true);
424
- // 调用停止流水线:NodePlatformDriver 真实后代树扫描探测到逃逸残留
442
+ // 等待采样轮询器(每 35ms 周期)至少完成一次后代树捕获
443
+ await new Promise((r) => setTimeout(r, 200));
444
+ // 调用停止流水线。结论必须与事实一致:
445
+ // - 能持有整棵树的驱动(如 Linux 子收割者)可以真的停掉逃逸者,此时逃逸者必须确已消失;
446
+ // - 做不到的驱动必须如实上报 cannot_determine + residualPids,绝不能声称 confirmed_stopped。
425
447
  const cancelRes = await supervisor.cancelOperation('op-c10-escape', 1000);
426
- // 真实上报 stopped: cannot_determine,且 residualPids 包含逃逸孤儿 PID
448
+ if (cancelRes.stopped === 'confirmed_stopped') {
449
+ expect(await waitUntilGone(escapedPid, 2000)).toBe(true);
450
+ const result = await executePromise;
451
+ expect(result.status).toBe('cancelled');
452
+ expect(domain.isResourceLocked('res:c10-lock')).toBe(false);
453
+ return;
454
+ }
427
455
  expect(cancelRes.stopped).toBe('cannot_determine');
428
456
  expect(cancelRes.residualPids).toBeDefined();
429
457
  expect(cancelRes.residualPids).toContain(escapedPid);
@@ -696,7 +724,7 @@ export function defineContractTestSuite(consumerName, createContext) {
696
724
  expect(throwing.stdout).toBe('captured-anyway');
697
725
  expect(throwing.streamCallbackError).toBe('consumer projection failed');
698
726
  });
699
- it('契约 19: 超时 + 逃逸后代持有管道,操作在有界时间内返回 indeterminate,不挂到逃逸进程退出', async () => {
727
+ it('契约 19: 超时 + 逃逸后代持有管道,操作在有界时间内结清,不挂到逃逸进程退出', async () => {
700
728
  const { supervisor, domain, tempDir } = ctx;
701
729
  ensureTaskAndRun(domain, 'task-c19', 'run-c19');
702
730
  const subPidFile = path.join(tempDir, 'sub-c19.pid');
@@ -726,13 +754,26 @@ export function defineContractTestSuite(consumerName, createContext) {
726
754
  });
727
755
  const duration = Date.now() - startedAt;
728
756
  expect(duration).toBeLessThan(3500);
729
- expect(result.status).toBe('indeterminate');
730
- expect(domain.isResourceLocked('res:c19-bounded')).toBe(true);
731
- // 清理逃逸孙进程
732
- if (fs.existsSync(subPidFile)) {
757
+ const subPid = fs.existsSync(subPidFile) ? parseInt(fs.readFileSync(subPidFile, 'utf8'), 10) : 0;
758
+ expect(subPid).toBeGreaterThan(0);
759
+ try {
760
+ // executeProcess 的声明类型不含 indeterminate,运行时结果可能是它
761
+ if (result.status === 'indeterminate') {
762
+ // 逃逸者未被确认停止:保留租约等待裁决
763
+ expect(domain.isResourceLocked('res:c19-bounded')).toBe(true);
764
+ }
765
+ else {
766
+ // 驱动确认停掉了整棵树:只有逃逸者确已消失才允许按超时失败结清并释放租约
767
+ expect(result.status).toBe('failed');
768
+ expect(result.terminationReason).toBe('timed_out');
769
+ expect(await waitUntilGone(subPid, 2000)).toBe(true);
770
+ expect(domain.isResourceLocked('res:c19-bounded')).toBe(false);
771
+ }
772
+ }
773
+ finally {
774
+ // 清理逃逸孙进程
733
775
  try {
734
- const pid = parseInt(fs.readFileSync(subPidFile, 'utf8'), 10);
735
- process.kill(pid, 'SIGKILL');
776
+ process.kill(subPid, 'SIGKILL');
736
777
  }
737
778
  catch { }
738
779
  }
@@ -802,6 +843,8 @@ export function defineContractTestSuite(consumerName, createContext) {
802
843
  expect(domain.isResourceLocked('res:c21')).toBe(false);
803
844
  const result = domain.getStore().getOperation(opId).result;
804
845
  expect(result.status).toBe('failed');
846
+ // 退出事实未被观察到:结局未知,不能被当作「确实失败、可换 opId 重试」
847
+ expect(result.terminationReason).toBe('exit_unobserved');
805
848
  expect(result.kind === 'process' ? result.stderr : '').toContain('reaped during recovery');
806
849
  }
807
850
  finally {
@@ -1163,6 +1206,570 @@ export function defineContractTestSuite(consumerName, createContext) {
1163
1206
  expect(res.replayed).toBe(true);
1164
1207
  expect(res.runId).toBe('run-c49');
1165
1208
  });
1209
+ it('契约 50: service 实例 stdinMode: "stream" 双向通信;stdout 直通不进内存保留;停止走标准流水线', async () => {
1210
+ const { supervisor, domain, tempDir } = ctx;
1211
+ const runId = 'run-c50';
1212
+ ensureTaskAndRun(domain, 'task-c50', runId);
1213
+ const script = `
1214
+ const readline = require('readline');
1215
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: false });
1216
+ rl.on('line', (line) => console.log('REPLY:' + line));
1217
+ `;
1218
+ const handle = await supervisor.startService({
1219
+ serviceId: 'service-c50',
1220
+ runId,
1221
+ command: {
1222
+ execPath: process.execPath,
1223
+ args: ['-e', script],
1224
+ cwd: tempDir,
1225
+ },
1226
+ readiness: 'spawned',
1227
+ });
1228
+ expect(handle.currentInstanceOpId).toBe('service-c50#1');
1229
+ await handle.ready;
1230
+ // 验证双向 stdin/stdout 流式交互
1231
+ const receivedLines = [];
1232
+ handle.stdout.on('data', (chunk) => {
1233
+ const lines = chunk.toString().split('\n');
1234
+ for (const l of lines) {
1235
+ if (l.trim())
1236
+ receivedLines.push(l.trim());
1237
+ }
1238
+ });
1239
+ handle.stdin.write('ping-50\n');
1240
+ const start = Date.now();
1241
+ while (!receivedLines.includes('REPLY:ping-50') && Date.now() - start < 3000) {
1242
+ await new Promise((r) => setTimeout(r, 20));
1243
+ }
1244
+ expect(receivedLines).toContain('REPLY:ping-50');
1245
+ // 停止 service 走标准流水线
1246
+ await handle.stop(1000);
1247
+ const events = domain.getStore().getJournalEvents(domain.domainId);
1248
+ const startEvt = events.find((e) => e.type === 'SERVICE_STARTED' && e.payload?.serviceId === 'service-c50');
1249
+ const readyEvt = events.find((e) => e.type === 'SERVICE_READY' && e.payload?.serviceId === 'service-c50');
1250
+ const stopEvt = events.find((e) => e.type === 'SERVICE_STOPPED' && e.payload?.serviceId === 'service-c50');
1251
+ expect(startEvt).toBeDefined();
1252
+ expect(readyEvt).toBeDefined();
1253
+ expect(stopEvt).toBeDefined();
1254
+ });
1255
+ it('契约 51: service on-failure 按退避重启且不超过上限;每次重启是新实例 op 并写 journal;超限 => failed', async () => {
1256
+ const { supervisor, domain, tempDir } = ctx;
1257
+ const runId = 'run-c51';
1258
+ ensureTaskAndRun(domain, 'task-c51', runId);
1259
+ const script = `
1260
+ const readline = require('readline');
1261
+ const rl = readline.createInterface({ input: process.stdin, terminal: false });
1262
+ rl.once('line', () => process.exit(42));
1263
+ `;
1264
+ const handle = await supervisor.startService({
1265
+ serviceId: 'service-c51',
1266
+ runId,
1267
+ command: {
1268
+ execPath: process.execPath,
1269
+ args: ['-e', script],
1270
+ cwd: tempDir,
1271
+ },
1272
+ readiness: 'spawned',
1273
+ restart: { policy: 'on-failure', maxRestarts: 2, backoffMs: 50 },
1274
+ });
1275
+ await handle.ready;
1276
+ expect(handle.currentInstanceOpId).toBe('service-c51#1');
1277
+ // 退出 instance #1
1278
+ handle.stdin.write('die-1\n');
1279
+ const start1 = Date.now();
1280
+ while (handle.currentInstanceOpId !== 'service-c51#2' && Date.now() - start1 < 3000) {
1281
+ await new Promise((r) => setTimeout(r, 20));
1282
+ }
1283
+ expect(handle.currentInstanceOpId).toBe('service-c51#2');
1284
+ // 退出 instance #2
1285
+ handle.stdin.write('die-2\n');
1286
+ const start2 = Date.now();
1287
+ while (handle.currentInstanceOpId !== 'service-c51#3' && Date.now() - start2 < 3000) {
1288
+ await new Promise((r) => setTimeout(r, 20));
1289
+ }
1290
+ expect(handle.currentInstanceOpId).toBe('service-c51#3');
1291
+ // 退出 instance #3(达到上限)
1292
+ handle.stdin.write('die-3\n');
1293
+ const start3 = Date.now();
1294
+ let events = domain.getStore().getJournalEvents(domain.domainId);
1295
+ while (!events.some((e) => e.type === 'SERVICE_FAILED' && e.payload?.serviceId === 'service-c51') && Date.now() - start3 < 3000) {
1296
+ await new Promise((r) => setTimeout(r, 20));
1297
+ events = domain.getStore().getJournalEvents(domain.domainId);
1298
+ }
1299
+ events = domain.getStore().getJournalEvents(domain.domainId);
1300
+ const restarts = events.filter((e) => e.type === 'SERVICE_RESTARTED' && e.payload?.serviceId === 'service-c51');
1301
+ expect(restarts.length).toBe(2);
1302
+ const failed = events.find((e) => e.type === 'SERVICE_FAILED' && e.payload?.serviceId === 'service-c51');
1303
+ expect(failed).toBeDefined();
1304
+ expect(failed?.payload?.maxRestartsExceeded).toBe(true);
1305
+ });
1306
+ it('契约 52: 宿主崩溃后 service 实例被核验并清场,恢复引擎不自动重启', async () => {
1307
+ const { supervisor, domain, driver, tempDir } = ctx;
1308
+ const runId = 'run-c52';
1309
+ ensureTaskAndRun(domain, 'task-c52', runId);
1310
+ const resource = 'res:c52-service';
1311
+ const script = `
1312
+ console.log('READY');
1313
+ setInterval(() => {}, 1000);
1314
+ `;
1315
+ const handle = await supervisor.startService({
1316
+ serviceId: 'service-c52',
1317
+ runId,
1318
+ command: { execPath: process.execPath, args: ['-e', script], cwd: tempDir },
1319
+ requiredResources: [resource],
1320
+ readiness: { stdoutLine: /^READY$/ },
1321
+ restart: { policy: 'on-failure', maxRestarts: 3, backoffMs: 50 },
1322
+ });
1323
+ await handle.ready;
1324
+ expect(domain.isResourceLocked(resource)).toBe(true);
1325
+ const op = domain.getStore().getOperation('service-c52#1');
1326
+ const childPid = op?.processIdentity?.pid;
1327
+ expect(childPid).toBeDefined();
1328
+ // 模拟宿主崩溃:关闭 domain,但不主动停止 service 实例
1329
+ domain.close();
1330
+ // 新宿主 acquire 并执行恢复
1331
+ const restartedDomain = ExecutionDomain.acquire(tempDir, domain.domainId);
1332
+ expect(restartedDomain.isResourceLocked(resource)).toBe(true);
1333
+ const recoveryEngine = new RecoveryEngine(restartedDomain, driver);
1334
+ const report = await recoveryEngine.recover();
1335
+ expect(report.recoveredServices).toBeDefined();
1336
+ const srvReport = report.recoveredServices.find((s) => s.serviceId === 'service-c52');
1337
+ expect(srvReport).toBeDefined();
1338
+ expect(srvReport?.resourcesReleased).toBe(true);
1339
+ expect(srvReport?.instanceOpIds).toContain('service-c52#1');
1340
+ // 资源租约已完全释放
1341
+ expect(restartedDomain.isResourceLocked(resource)).toBe(false);
1342
+ // 进程已停止
1343
+ let isAlive = true;
1344
+ try {
1345
+ process.kill(childPid, 0);
1346
+ }
1347
+ catch (err) {
1348
+ if (err.code === 'ESRCH')
1349
+ isAlive = false;
1350
+ }
1351
+ expect(isAlive).toBe(false);
1352
+ // 不会自动重启新实例
1353
+ await new Promise((r) => setTimeout(r, 150));
1354
+ const allOps = restartedDomain.getStore().getOperationsByRun(runId);
1355
+ expect(allOps.find((o) => o.id === 'service-c52#2')).toBeUndefined();
1356
+ restartedDomain.close();
1357
+ });
1358
+ it('契约 28: 回滚后指纹核验:restored / partial / failed 如实区分', async () => {
1359
+ const { supervisor, domain, tempDir } = ctx;
1360
+ const repoDir = path.join(tempDir, 'repo-c28');
1361
+ fs.mkdirSync(repoDir, { recursive: true });
1362
+ await execFileAsync('git', ['init', '-b', 'main'], { cwd: repoDir });
1363
+ await execFileAsync('git', ['config', 'user.name', 'Tester'], { cwd: repoDir });
1364
+ await execFileAsync('git', ['config', 'user.email', 'tester@test.local'], { cwd: repoDir });
1365
+ fs.writeFileSync(path.join(repoDir, 'main.ts'), 'version 1;\n');
1366
+ await execFileAsync('git', ['add', '.'], { cwd: repoDir });
1367
+ await execFileAsync('git', ['commit', '-m', 'v1'], { cwd: repoDir });
1368
+ ensureTaskAndRun(domain, 'task-c28', 'run-c28');
1369
+ // 捕获快照
1370
+ const snapRes = await supervisor.captureSnapshot({
1371
+ runId: 'run-c28',
1372
+ opId: 'op-c28-snap',
1373
+ roots: [repoDir],
1374
+ });
1375
+ expect(snapRes.status).toBe('succeeded');
1376
+ const snapId = snapRes.snapshot.id;
1377
+ // 修改工作区文件并增加新文件
1378
+ fs.writeFileSync(path.join(repoDir, 'main.ts'), 'version 2 modified;\n');
1379
+ fs.writeFileSync(path.join(repoDir, 'extra.txt'), 'extra untracked\n');
1380
+ // 执行回滚
1381
+ const rbRes = await supervisor.rollback({
1382
+ runId: 'run-c28',
1383
+ opId: 'op-c28-rollback-1',
1384
+ snapshotId: snapId,
1385
+ });
1386
+ expect(rbRes.status).toBe('restored');
1387
+ expect(fs.readFileSync(path.join(repoDir, 'main.ts'), 'utf8')).toBe('version 1;\n');
1388
+ expect(fs.existsSync(path.join(repoDir, 'extra.txt'))).toBe(false);
1389
+ expect(domain.isResourceLocked(`workspace:write:${path.resolve(repoDir)}`)).toBe(false);
1390
+ });
1391
+ it('契约 29: 无写入限制时回滚 coverage 不得为 complete,outOfScopeEffects = "possible"', async () => {
1392
+ const { supervisor, domain, tempDir } = ctx;
1393
+ const repoDir = path.join(tempDir, 'repo-c29');
1394
+ fs.mkdirSync(repoDir, { recursive: true });
1395
+ await execFileAsync('git', ['init', '-b', 'main'], { cwd: repoDir });
1396
+ await execFileAsync('git', ['config', 'user.name', 'Tester'], { cwd: repoDir });
1397
+ await execFileAsync('git', ['config', 'user.email', 'tester@test.local'], { cwd: repoDir });
1398
+ fs.writeFileSync(path.join(repoDir, 'code.js'), 'let x = 1;\n');
1399
+ await execFileAsync('git', ['add', '.'], { cwd: repoDir });
1400
+ await execFileAsync('git', ['commit', '-m', 'init'], { cwd: repoDir });
1401
+ ensureTaskAndRun(domain, 'task-c29', 'run-c29');
1402
+ const snapRes = await supervisor.captureSnapshot({
1403
+ runId: 'run-c29',
1404
+ opId: 'op-c29-snap',
1405
+ roots: [repoDir],
1406
+ });
1407
+ const snapId = snapRes.snapshot.id;
1408
+ // 执行一个普通不受限的 process 操作
1409
+ await supervisor.executeProcess({
1410
+ runId: 'run-c29',
1411
+ opId: 'op-c29-unconfined',
1412
+ name: 'unconfined-cmd',
1413
+ command: { execPath: process.execPath, args: ['-e', 'console.log(123)'], cwd: repoDir },
1414
+ requiredResources: [],
1415
+ });
1416
+ // 回滚
1417
+ const rbRes = await supervisor.rollback({
1418
+ runId: 'run-c29',
1419
+ opId: 'op-c29-rollback',
1420
+ snapshotId: snapId,
1421
+ });
1422
+ // 契约 #29 严禁在无写入限制时声明 complete
1423
+ expect(rbRes.coverage).not.toBe('complete');
1424
+ expect(rbRes.coverage).toBe('declared_roots');
1425
+ expect(rbRes.outOfScopeEffects).toBe('possible');
1426
+ });
1427
+ it('契约 30: 回滚与目标根目录上的活跃 / 未裁决操作互斥', async () => {
1428
+ const { supervisor, domain, tempDir } = ctx;
1429
+ const repoDir = path.join(tempDir, 'repo-c30');
1430
+ fs.mkdirSync(repoDir, { recursive: true });
1431
+ await execFileAsync('git', ['init', '-b', 'main'], { cwd: repoDir });
1432
+ await execFileAsync('git', ['config', 'user.name', 'Tester'], { cwd: repoDir });
1433
+ await execFileAsync('git', ['config', 'user.email', 'tester@test.local'], { cwd: repoDir });
1434
+ fs.writeFileSync(path.join(repoDir, 'index.js'), '// initial\n');
1435
+ await execFileAsync('git', ['add', '.'], { cwd: repoDir });
1436
+ await execFileAsync('git', ['commit', '-m', 'init'], { cwd: repoDir });
1437
+ ensureTaskAndRun(domain, 'task-c30', 'run-c30');
1438
+ const snapRes = await supervisor.captureSnapshot({
1439
+ runId: 'run-c30',
1440
+ opId: 'op-c30-snap',
1441
+ roots: [repoDir],
1442
+ });
1443
+ const snapId = snapRes.snapshot.id;
1444
+ // 在目标根上持有写租约并启动长时间操作
1445
+ const writeRes = `workspace:write:${path.resolve(repoDir)}`;
1446
+ const activePromise = supervisor.executeProcess({
1447
+ runId: 'run-c30',
1448
+ opId: 'op-c30-holding',
1449
+ name: 'holder',
1450
+ command: { execPath: process.execPath, args: ['-e', 'setTimeout(() => {}, 2000)'], cwd: repoDir },
1451
+ requiredResources: [writeRes],
1452
+ });
1453
+ // 等待进程启动获得租约
1454
+ const normRes = normalizeResourceName(writeRes);
1455
+ while (!domain.isResourceLocked(normRes)) {
1456
+ await new Promise((r) => setTimeout(r, 10));
1457
+ }
1458
+ // 尝试在活跃操作持有目标目录时回滚,必须被拒绝并给出持有者诊断
1459
+ await expect(supervisor.rollback({
1460
+ runId: 'run-c30',
1461
+ opId: 'op-c30-rollback-conflict',
1462
+ snapshotId: snapId,
1463
+ })).rejects.toThrow(ResourceConflictError);
1464
+ await supervisor.cancelOperation('op-c30-holding');
1465
+ await activePromise;
1466
+ });
1467
+ it('契约 31: 回滚中途崩溃:恢复按指纹判定 restored 或 indeterminate', async () => {
1468
+ const { supervisor, domain, driver, tempDir } = ctx;
1469
+ const repoDir = path.join(tempDir, 'repo-c31');
1470
+ fs.mkdirSync(repoDir, { recursive: true });
1471
+ await execFileAsync('git', ['init', '-b', 'main'], { cwd: repoDir });
1472
+ await execFileAsync('git', ['config', 'user.name', 'Tester'], { cwd: repoDir });
1473
+ await execFileAsync('git', ['config', 'user.email', 'tester@test.local'], { cwd: repoDir });
1474
+ fs.writeFileSync(path.join(repoDir, 'tracked.txt'), 'clean v1\n');
1475
+ await execFileAsync('git', ['add', '.'], { cwd: repoDir });
1476
+ await execFileAsync('git', ['commit', '-m', 'clean v1'], { cwd: repoDir });
1477
+ ensureTaskAndRun(domain, 'task-c31', 'run-c31');
1478
+ const snapRes = await supervisor.captureSnapshot({
1479
+ runId: 'run-c31',
1480
+ opId: 'op-c31-snap',
1481
+ roots: [repoDir],
1482
+ });
1483
+ const snapId = snapRes.snapshot.id;
1484
+ // 场景 A: 模拟回滚中途宿主崩溃,但工作区指纹与快照一致
1485
+ const writeRes = `workspace:write:${path.resolve(repoDir)}`;
1486
+ domain.getStore().registerOperationIntent({
1487
+ id: 'op-c31-crash-match',
1488
+ runId: 'run-c31',
1489
+ kind: 'rollback',
1490
+ name: `rollback:${snapId}`,
1491
+ inputFingerprint: 'fp-c31',
1492
+ requiredResources: [writeRes],
1493
+ mutationRoots: [path.resolve(repoDir)],
1494
+ outputRef: snapId,
1495
+ status: 'intent_registered',
1496
+ }, domain.domainId);
1497
+ domain.getStore().updateOperationStatus('op-c31-crash-match', 'active');
1498
+ domain.allocateResources('op-c31-crash-match', [writeRes]);
1499
+ const recoveryA = new RecoveryEngine(domain, driver);
1500
+ const repA = await recoveryA.recover();
1501
+ const recA = repA.recoveredOperations.find((o) => o.opId === 'op-c31-crash-match');
1502
+ expect(recA?.resourcesReleased).toBe(true);
1503
+ const opA = domain.getStore().getOperation('op-c31-crash-match');
1504
+ expect(opA?.result?.status).toBe('restored');
1505
+ expect(domain.isResourceLocked(writeRes)).toBe(false);
1506
+ // 场景 B: 模拟回滚中途宿主崩溃,且工作区被修改导致指纹不匹配
1507
+ ensureTaskAndRun(domain, 'task-c31', 'run-c31-b');
1508
+ fs.writeFileSync(path.join(repoDir, 'tracked.txt'), 'corrupted mid-rollback\n');
1509
+ domain.getStore().registerOperationIntent({
1510
+ id: 'op-c31-crash-mismatch',
1511
+ runId: 'run-c31-b',
1512
+ kind: 'rollback',
1513
+ name: `rollback:${snapId}`,
1514
+ inputFingerprint: 'fp-c31-b',
1515
+ requiredResources: [writeRes],
1516
+ mutationRoots: [path.resolve(repoDir)],
1517
+ outputRef: snapId,
1518
+ status: 'intent_registered',
1519
+ }, domain.domainId);
1520
+ domain.getStore().updateOperationStatus('op-c31-crash-mismatch', 'active');
1521
+ domain.allocateResources('op-c31-crash-mismatch', [writeRes]);
1522
+ const repB = await recoveryA.recover();
1523
+ const recB = repB.recoveredOperations.find((o) => o.opId === 'op-c31-crash-mismatch');
1524
+ expect(recB?.resourcesReleased).toBe(false);
1525
+ const opB = domain.getStore().getOperation('op-c31-crash-mismatch');
1526
+ expect(opB?.result?.status).toBe('indeterminate');
1527
+ expect(domain.isResourceLocked(writeRes)).toBe(true);
1528
+ // 清理资源
1529
+ domain.internalReleaseResources('op-c31-crash-mismatch', [writeRes]);
1530
+ });
1531
+ it('契约 32: git-shadow 快照不改动用户 index、HEAD 与分支,并如实声明 worktree_non_ignored', async () => {
1532
+ const { supervisor, domain, tempDir } = ctx;
1533
+ const repoDir = path.join(tempDir, 'repo-c32');
1534
+ fs.mkdirSync(repoDir, { recursive: true });
1535
+ await execFileAsync('git', ['init', '-b', 'main'], { cwd: repoDir });
1536
+ await execFileAsync('git', ['config', 'user.name', 'Tester'], { cwd: repoDir });
1537
+ await execFileAsync('git', ['config', 'user.email', 'tester@test.local'], { cwd: repoDir });
1538
+ fs.writeFileSync(path.join(repoDir, 'base.txt'), 'base\n');
1539
+ fs.writeFileSync(path.join(repoDir, '.gitignore'), '.env\n');
1540
+ await execFileAsync('git', ['add', '.'], { cwd: repoDir });
1541
+ await execFileAsync('git', ['commit', '-m', 'base commit'], { cwd: repoDir });
1542
+ // 构造用户正在工作的工作区现场
1543
+ fs.writeFileSync(path.join(repoDir, 'staged.txt'), 'user staged change\n');
1544
+ await execFileAsync('git', ['add', 'staged.txt'], { cwd: repoDir });
1545
+ fs.writeFileSync(path.join(repoDir, 'unstaged.txt'), 'user unstaged change\n');
1546
+ fs.writeFileSync(path.join(repoDir, '.env'), 'SECRET=donttouch\n');
1547
+ const statusBefore = (await execFileAsync('git', ['status', '--porcelain'], { cwd: repoDir })).stdout;
1548
+ const stageBefore = (await execFileAsync('git', ['ls-files', '--stage'], { cwd: repoDir })).stdout;
1549
+ const headBefore = (await execFileAsync('git', ['rev-parse', 'HEAD'], { cwd: repoDir })).stdout;
1550
+ const branchBefore = (await execFileAsync('git', ['branch'], { cwd: repoDir })).stdout;
1551
+ ensureTaskAndRun(domain, 'task-c32', 'run-c32');
1552
+ const snapRes = await supervisor.captureSnapshot({
1553
+ runId: 'run-c32',
1554
+ opId: 'op-c32',
1555
+ roots: [repoDir],
1556
+ });
1557
+ expect(snapRes.status).toBe('succeeded');
1558
+ expect(snapRes.snapshot?.coverage).toBe('worktree_non_ignored');
1559
+ const statusAfter = (await execFileAsync('git', ['status', '--porcelain'], { cwd: repoDir })).stdout;
1560
+ const stageAfter = (await execFileAsync('git', ['ls-files', '--stage'], { cwd: repoDir })).stdout;
1561
+ const headAfter = (await execFileAsync('git', ['rev-parse', 'HEAD'], { cwd: repoDir })).stdout;
1562
+ const branchAfter = (await execFileAsync('git', ['branch'], { cwd: repoDir })).stdout;
1563
+ expect(statusAfter).toBe(statusBefore);
1564
+ expect(stageAfter).toBe(stageBefore);
1565
+ expect(headAfter).toBe(headBefore);
1566
+ expect(branchAfter).toBe(branchBefore);
1567
+ });
1568
+ it('契约 53: SnapshotRef.journalSeq 与捕获事件 seq 一致;可按 seq 查询不晚于它的最近快照', async () => {
1569
+ const { supervisor, domain, tempDir } = ctx;
1570
+ const repoDir = path.join(tempDir, 'repo-c53');
1571
+ fs.mkdirSync(repoDir, { recursive: true });
1572
+ await execFileAsync('git', ['init', '-b', 'main'], { cwd: repoDir });
1573
+ await execFileAsync('git', ['config', 'user.name', 'Tester'], { cwd: repoDir });
1574
+ await execFileAsync('git', ['config', 'user.email', 'tester@test.local'], { cwd: repoDir });
1575
+ fs.writeFileSync(path.join(repoDir, 'step.txt'), 'step 0\n');
1576
+ await execFileAsync('git', ['add', '.'], { cwd: repoDir });
1577
+ await execFileAsync('git', ['commit', '-m', 'step 0'], { cwd: repoDir });
1578
+ ensureTaskAndRun(domain, 'task-c53', 'run-c53');
1579
+ // 捕获第 1 个快照
1580
+ const snap1 = await supervisor.captureSnapshot({
1581
+ runId: 'run-c53',
1582
+ opId: 'snap-1',
1583
+ roots: [repoDir],
1584
+ });
1585
+ const events1 = domain.getStore().getJournalEvents(domain.domainId);
1586
+ const capEvent1 = events1.find((e) => e.type === 'SNAPSHOT_CAPTURED' && e.payload?.snapshotId === 'snap-1');
1587
+ expect(capEvent1).toBeDefined();
1588
+ expect(snap1.snapshot?.journalSeq).toBe(capEvent1?.seq);
1589
+ fs.writeFileSync(path.join(repoDir, 'step.txt'), 'step 1\n');
1590
+ // 捕获第 2 个快照
1591
+ const snap2 = await supervisor.captureSnapshot({
1592
+ runId: 'run-c53',
1593
+ opId: 'snap-2',
1594
+ roots: [repoDir],
1595
+ });
1596
+ const events2 = domain.getStore().getJournalEvents(domain.domainId);
1597
+ const capEvent2 = events2.find((e) => e.type === 'SNAPSHOT_CAPTURED' && e.payload?.snapshotId === 'snap-2');
1598
+ expect(snap2.snapshot?.journalSeq).toBe(capEvent2?.seq);
1599
+ // 查询 seq 不晚于 capEvent1.seq 的快照 => 必须返回 snap-1
1600
+ const foundSnap1 = domain.getStore().findSnapshotAtOrBefore(domain.domainId, capEvent1.seq);
1601
+ expect(foundSnap1?.id).toBe('snap-1');
1602
+ // 查询 seq 不晚于 capEvent2.seq 的快照 => 必须返回 snap-2
1603
+ const foundSnap2 = domain.getStore().findSnapshotAtOrBefore(domain.domainId, capEvent2.seq);
1604
+ expect(foundSnap2?.id).toBe('snap-2');
1605
+ });
1606
+ it('契约 54: materialize 生成的工作区独立;源工作区、用户 index、HEAD 与分支不变;dematerialize 后无残留 worktree', async () => {
1607
+ const { supervisor, domain, tempDir } = ctx;
1608
+ const repoDir = path.join(tempDir, 'repo-c54');
1609
+ fs.mkdirSync(repoDir, { recursive: true });
1610
+ await execFileAsync('git', ['init', '-b', 'main'], { cwd: repoDir });
1611
+ await execFileAsync('git', ['config', 'user.name', 'Tester'], { cwd: repoDir });
1612
+ await execFileAsync('git', ['config', 'user.email', 'tester@test.local'], { cwd: repoDir });
1613
+ fs.writeFileSync(path.join(repoDir, 'entry.ts'), 'export const val = 42;\n');
1614
+ await execFileAsync('git', ['add', '.'], { cwd: repoDir });
1615
+ await execFileAsync('git', ['commit', '-m', 'entry commit'], { cwd: repoDir });
1616
+ ensureTaskAndRun(domain, 'task-c54', 'run-c54');
1617
+ const snapRes = await supervisor.captureSnapshot({
1618
+ runId: 'run-c54',
1619
+ opId: 'snap-c54',
1620
+ roots: [repoDir],
1621
+ });
1622
+ const forkDir = path.join(tempDir, 'fork-c54');
1623
+ const { worktreePath } = await supervisor.materialize('snap-c54', forkDir);
1624
+ expect(worktreePath).toBe(forkDir);
1625
+ expect(fs.existsSync(path.join(forkDir, 'entry.ts'))).toBe(true);
1626
+ expect(domain.isResourceLocked(`workspace:write:${path.resolve(forkDir)}`)).toBe(true);
1627
+ // dematerialize
1628
+ await supervisor.dematerialize(forkDir);
1629
+ expect(fs.existsSync(forkDir)).toBe(false);
1630
+ expect(domain.isResourceLocked(`workspace:write:${path.resolve(forkDir)}`)).toBe(false);
1631
+ const { stdout: wtList } = await execFileAsync('git', ['worktree', 'list'], { cwd: repoDir });
1632
+ expect(wtList).not.toContain(forkDir);
1633
+ });
1634
+ it('契约 55: 准入校验:过期 / 撤销 / 旧 epoch / 越界资源 / 越界路径抛 CapabilityViolationError 并记 CAPABILITY_REJECTED;缺省推导生效并记 CAPABILITY_USED', async () => {
1635
+ const { supervisor, domain, tempDir } = ctx;
1636
+ ensureTaskAndRun(domain, 'task-c55', 'run-c55');
1637
+ const allowedDir = path.join(tempDir, 'allowed');
1638
+ const outsideDir = path.join(tempDir, 'outside');
1639
+ fs.mkdirSync(allowedDir, { recursive: true });
1640
+ fs.mkdirSync(outsideDir, { recursive: true });
1641
+ // 1. 签发有效 capability
1642
+ const cap = domain.issueCapability({
1643
+ write: [allowedDir],
1644
+ exclusive: ['res:allowed-1'],
1645
+ }, 'tester', 60000);
1646
+ // 2. 越界资源
1647
+ await expect(supervisor.executeProcess({
1648
+ runId: 'run-c55',
1649
+ opId: 'op-c55-res-violation',
1650
+ name: 'violating-proc',
1651
+ command: { execPath: process.execPath, args: ['-e', 'process.exit(0)'], cwd: allowedDir },
1652
+ requiredResources: ['res:unauthorized-res'],
1653
+ capabilityId: cap.id,
1654
+ })).rejects.toThrowError(CapabilityViolationError);
1655
+ // 3. 越界路径 (cwd 在 allowedDir 之外)
1656
+ await expect(supervisor.executeProcess({
1657
+ runId: 'run-c55',
1658
+ opId: 'op-c55-path-violation',
1659
+ name: 'violating-path',
1660
+ command: { execPath: process.execPath, args: ['-e', 'process.exit(0)'], cwd: outsideDir },
1661
+ capabilityId: cap.id,
1662
+ })).rejects.toThrowError(CapabilityViolationError);
1663
+ // 4. 撤销 capability
1664
+ domain.revokeCapability(cap.id, 'test_revocation');
1665
+ await expect(supervisor.executeProcess({
1666
+ runId: 'run-c55',
1667
+ opId: 'op-c55-revoked',
1668
+ name: 'revoked-proc',
1669
+ command: { execPath: process.execPath, args: ['-e', 'process.exit(0)'], cwd: allowedDir },
1670
+ capabilityId: cap.id,
1671
+ })).rejects.toThrowError(CapabilityViolationError);
1672
+ // 验证 CAPABILITY_REJECTED 事件已入 journal
1673
+ const events = domain.getStore().getJournalEvents(domain.domainId);
1674
+ const rejectedEvents = events.filter((e) => e.type === 'CAPABILITY_REJECTED');
1675
+ expect(rejectedEvents.length).toBeGreaterThanOrEqual(3);
1676
+ // 5. 缺省 capability 推导生效与 CAPABILITY_USED 记录
1677
+ const validCap = domain.issueCapability({
1678
+ write: [tempDir],
1679
+ exclusive: ['res:default-1'],
1680
+ }, 'tester', 60000);
1681
+ const res = await supervisor.executeProcess({
1682
+ runId: 'run-c55',
1683
+ opId: 'op-c55-success',
1684
+ name: 'valid-proc',
1685
+ command: { execPath: process.execPath, args: ['-e', 'process.exit(0)'], cwd: tempDir },
1686
+ requiredResources: ['res:default-1'],
1687
+ capabilityId: validCap.id,
1688
+ });
1689
+ expect(res.status).toBe('succeeded');
1690
+ expect(res.capabilityId).toBe(validCap.id);
1691
+ const usedEvents = domain.getStore().getJournalEvents(domain.domainId).filter((e) => e.type === 'CAPABILITY_USED');
1692
+ expect(usedEvents.some((e) => e.payload?.capabilityId === validCap.id)).toBe(true);
1693
+ });
1694
+ it('契约 56: 写入限制驱动:受限写外部路径失败;快照后全受限回滚声明 complete,混入不受限声明 declared_roots', async () => {
1695
+ const { supervisor, domain, tempDir } = ctx;
1696
+ const repoDir = path.join(tempDir, 'repo-c56');
1697
+ fs.mkdirSync(repoDir, { recursive: true });
1698
+ await execFileAsync('git', ['init', '-b', 'main'], { cwd: repoDir });
1699
+ await execFileAsync('git', ['config', 'user.name', 'Tester'], { cwd: repoDir });
1700
+ await execFileAsync('git', ['config', 'user.email', 'tester@test.local'], { cwd: repoDir });
1701
+ fs.writeFileSync(path.join(repoDir, 'init.txt'), 'init\n');
1702
+ await execFileAsync('git', ['add', '.'], { cwd: repoDir });
1703
+ await execFileAsync('git', ['commit', '-m', 'init'], { cwd: repoDir });
1704
+ ensureTaskAndRun(domain, 'task-c56', 'run-c56');
1705
+ const mockDriver = {
1706
+ name: 'mock-confinement',
1707
+ wrap: (cmd) => cmd,
1708
+ };
1709
+ // 1. 签发受限 capability (针对 repoDir)
1710
+ const confinedCap = domain.issueCapability({
1711
+ write: [repoDir],
1712
+ exclusive: [`workspace:write:${path.resolve(repoDir)}`],
1713
+ }, 'tester', 60000);
1714
+ // 捕获快照
1715
+ const snapRes = await supervisor.captureSnapshot({
1716
+ runId: 'run-c56',
1717
+ opId: 'snap-c56',
1718
+ capabilityId: confinedCap.id,
1719
+ });
1720
+ expect(snapRes.status).toBe('succeeded');
1721
+ // 在受限下运行进程写文件
1722
+ const p1 = await supervisor.executeProcess({
1723
+ runId: 'run-c56',
1724
+ opId: 'op-c56-confined',
1725
+ name: 'write-in-repo',
1726
+ command: {
1727
+ execPath: process.execPath,
1728
+ args: ['-e', `require('node:fs').writeFileSync(${JSON.stringify(path.join(repoDir, 'a.txt'))}, 'a');`],
1729
+ cwd: repoDir,
1730
+ },
1731
+ capabilityId: confinedCap.id,
1732
+ confinementDriver: mockDriver,
1733
+ });
1734
+ expect(p1.status).toBe('succeeded');
1735
+ expect(p1.confined).toBe(true);
1736
+ // 全部 op 受限,回滚 coverage 必须为 complete
1737
+ const rollback1 = await supervisor.rollback({
1738
+ runId: 'run-c56',
1739
+ opId: 'rb-c56-1',
1740
+ snapshotId: 'snap-c56',
1741
+ capabilityId: confinedCap.id,
1742
+ });
1743
+ expect(rollback1.coverage).toBe('complete');
1744
+ expect(rollback1.outOfScopeEffects).toBe('none_possible');
1745
+ // 2. 混入不受限操作
1746
+ ensureTaskAndRun(domain, 'task-c56', 'run-c56-unconfined');
1747
+ const snap2 = await supervisor.captureSnapshot({
1748
+ runId: 'run-c56-unconfined',
1749
+ opId: 'snap-c56-2',
1750
+ roots: [repoDir],
1751
+ });
1752
+ const p2 = await supervisor.executeProcess({
1753
+ runId: 'run-c56-unconfined',
1754
+ opId: 'op-c56-unconfined',
1755
+ name: 'write-unconfined',
1756
+ command: {
1757
+ execPath: process.execPath,
1758
+ args: ['-e', `require('node:fs').writeFileSync(${JSON.stringify(path.join(repoDir, 'b.txt'))}, 'b');`],
1759
+ cwd: repoDir,
1760
+ },
1761
+ requiredResources: [`workspace:write:${path.resolve(repoDir)}`],
1762
+ });
1763
+ expect(p2.confined).toBeFalsy();
1764
+ // 混入不受限操作,回滚 coverage 退化为 declared_roots
1765
+ const rollback2 = await supervisor.rollback({
1766
+ runId: 'run-c56-unconfined',
1767
+ opId: 'rb-c56-2',
1768
+ snapshotId: 'snap-c56-2',
1769
+ });
1770
+ expect(rollback2.coverage).toBe('declared_roots');
1771
+ expect(rollback2.outOfScopeEffects).toBe('possible');
1772
+ });
1166
1773
  });
1167
1774
  }
1168
1775
  //# sourceMappingURL=contract-suite.js.map