@namewta/speculo 1.0.5 → 1.0.7

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 (151) hide show
  1. package/README.md +1 -1
  2. package/dist/src/kernel.d.ts +23 -2
  3. package/dist/src/kernel.js +9 -0
  4. package/dist/src/kernel.js.map +1 -1
  5. package/dist/src/ops-resources.d.ts +3 -0
  6. package/dist/src/ops-resources.js +188 -0
  7. package/dist/src/ops-resources.js.map +1 -0
  8. package/dist/src/refresh.js +20 -2
  9. package/dist/src/refresh.js.map +1 -1
  10. package/dist/src/structured.js +7 -0
  11. package/dist/src/structured.js.map +1 -1
  12. package/dist/src/workflows.js +1 -1
  13. package/dist/src/workflows.js.map +1 -1
  14. package/package.json +1 -1
  15. package/template/.speculo/README.md +3 -3
  16. package/template/.speculo/kernel/README.md +3 -0
  17. package/template/.speculo/kernel/checkpoint.schema.json +138 -4
  18. package/template/commands/archive-and-consolidate.md +5 -1
  19. package/template/commands/status.md +2 -2
  20. package/template/workflows/ops/D-project-deploy/D-project-deploy.md +34 -0
  21. package/template/workflows/ops/H-host-manage/H-host-manage.md +30 -0
  22. package/template/workflows/ops/I-initialize/I-initialize.md +28 -0
  23. package/template/workflows/ops/INDEX.md +15 -17
  24. package/template/workflows/ops/README.md +59 -93
  25. package/template/workflows/ops/_state/status.json +15 -3
  26. package/template/workflows/ops/common/CAPABILITIES.md +23 -0
  27. package/template/workflows/ops/common/USAGE.md +112 -0
  28. package/template/workflows/ops/common/examples/README.md +7 -0
  29. package/template/workflows/ops/common/examples/binding.example.json +12 -0
  30. package/template/workflows/ops/common/examples/compose-app.example.json +56 -0
  31. package/template/workflows/ops/common/examples/credential.example.json +9 -0
  32. package/template/workflows/ops/common/examples/environment-request.example.json +7 -0
  33. package/template/workflows/ops/common/examples/register.example.json +44 -0
  34. package/template/workflows/ops/common/examples/shared-allocation.example.json +37 -0
  35. package/template/workflows/ops/common/rules/activation-and-memory.md +5 -20
  36. package/template/workflows/ops/common/rules/persistence-and-secrets.md +27 -0
  37. package/template/workflows/ops/common/rules/recovery.md +15 -0
  38. package/template/workflows/ops/common/rules/shared-services.md +13 -0
  39. package/template/workflows/ops/common/schemas/allocation.schema.json +92 -0
  40. package/template/workflows/ops/common/schemas/approval.schema.json +41 -21
  41. package/template/workflows/ops/common/schemas/binding.schema.json +73 -0
  42. package/template/workflows/ops/common/schemas/deployment.schema.json +244 -0
  43. package/template/workflows/ops/common/schemas/host.schema.json +88 -0
  44. package/template/workflows/ops/common/schemas/plan.schema.json +1573 -0
  45. package/template/workflows/ops/common/schemas/project.schema.json +55 -11
  46. package/template/workflows/ops/common/schemas/spec.schema.json +1032 -0
  47. package/template/workflows/ops/common/schemas/status.schema.json +758 -17
  48. package/template/workflows/ops/common/service-profiles/custom.md +5 -0
  49. package/template/workflows/ops/common/service-profiles/docker-engine.md +11 -0
  50. package/template/workflows/ops/common/service-profiles/minio.md +7 -0
  51. package/template/workflows/ops/common/service-profiles/mysql.md +7 -0
  52. package/template/workflows/ops/common/service-profiles/redis.md +7 -0
  53. package/template/workflows/ops/common/templates/CONTROLLER-RECORD.md +13 -0
  54. package/template/workflows/ops/common/templates/EXECUTION-PLAN.md +15 -0
  55. package/template/workflows/ops/common/templates/HOST-README.md +11 -0
  56. package/template/workflows/ops/common/templates/PROJECT-README.md +15 -0
  57. package/template/workflows/ops/common/tests/test_ops.mjs +752 -0
  58. package/template/workflows/ops/common/tools/bootstrap.ps1 +18 -0
  59. package/template/workflows/ops/common/tools/bootstrap.sh +27 -0
  60. package/template/workflows/ops/common/tools/demo-local.mjs +101 -0
  61. package/template/workflows/ops/common/tools/ops.mjs +4 -0
  62. package/template/workflows/ops/common/tools/opslib/agent.mjs +873 -0
  63. package/template/workflows/ops/common/tools/opslib/cli.mjs +311 -0
  64. package/template/workflows/ops/common/tools/opslib/core.mjs +393 -0
  65. package/template/workflows/ops/common/tools/opslib/docs.mjs +252 -0
  66. package/template/workflows/ops/common/tools/opslib/execution.mjs +378 -0
  67. package/template/workflows/ops/common/tools/opslib/host_recipes.mjs +109 -0
  68. package/template/workflows/ops/common/tools/opslib/model.mjs +272 -0
  69. package/template/workflows/ops/common/tools/opslib/native_windows.mjs +36 -0
  70. package/template/workflows/ops/common/tools/opslib/planner.mjs +687 -0
  71. package/template/workflows/ops/common/tools/opslib/services.mjs +76 -0
  72. package/template/workflows/ops/common/tools/opslib/sources.mjs +56 -0
  73. package/template/workflows/ops/common/tools/opslib/transport.mjs +127 -0
  74. package/template/workflows/ops/common/tools/validate-ops.mjs +43 -1012
  75. package/template/workflows/ops/manifest.json +60 -1
  76. package/template/workflows/ops/runtime-contract.json +1 -6
  77. package/template/workflows/ops/A-archive-and-learn/A-archive-and-learn.md +0 -73
  78. package/template/workflows/ops/A-archive-and-learn/promotion-plan-template.md +0 -41
  79. package/template/workflows/ops/A-archive-and-learn/retrospective-template.md +0 -53
  80. package/template/workflows/ops/E-execute-and-stabilize/E-execute-and-stabilize.md +0 -82
  81. package/template/workflows/ops/E-execute-and-stabilize/attempt-summary-template.md +0 -44
  82. package/template/workflows/ops/E-execute-and-stabilize/diagnosis-template.md +0 -24
  83. package/template/workflows/ops/E-execute-and-stabilize/handoff-template.md +0 -32
  84. package/template/workflows/ops/E-execute-and-stabilize/rollback-template.md +0 -26
  85. package/template/workflows/ops/E-execute-and-stabilize/verification-state-template.json +0 -30
  86. package/template/workflows/ops/E-execute-and-stabilize/verification-template.md +0 -55
  87. package/template/workflows/ops/H-computer-hygiene/H-computer-hygiene.md +0 -105
  88. package/template/workflows/ops/H-computer-hygiene/examples/README.md +0 -9
  89. package/template/workflows/ops/H-computer-hygiene/examples/linux/2026-09-14.environment.md +0 -45
  90. package/template/workflows/ops/H-computer-hygiene/examples/linux/2026-09-14.json +0 -970
  91. package/template/workflows/ops/H-computer-hygiene/examples/linux/2026-09-14.md +0 -216
  92. package/template/workflows/ops/H-computer-hygiene/examples/macos/2026-09-14.environment.md +0 -45
  93. package/template/workflows/ops/H-computer-hygiene/examples/macos/2026-09-14.json +0 -970
  94. package/template/workflows/ops/H-computer-hygiene/examples/macos/2026-09-14.md +0 -216
  95. package/template/workflows/ops/H-computer-hygiene/examples/windows/2026-09-14.environment.md +0 -45
  96. package/template/workflows/ops/H-computer-hygiene/examples/windows/2026-09-14.json +0 -969
  97. package/template/workflows/ops/H-computer-hygiene/examples/windows/2026-09-14.md +0 -216
  98. package/template/workflows/ops/H-computer-hygiene/references/00-sources.md +0 -58
  99. package/template/workflows/ops/H-computer-hygiene/references/01-safety.md +0 -52
  100. package/template/workflows/ops/H-computer-hygiene/references/10-windows.md +0 -47
  101. package/template/workflows/ops/H-computer-hygiene/references/11-macos.md +0 -46
  102. package/template/workflows/ops/H-computer-hygiene/references/12-linux.md +0 -13
  103. package/template/workflows/ops/H-computer-hygiene/references/20-toolchains.md +0 -83
  104. package/template/workflows/ops/H-computer-hygiene/references/21-path-migration.md +0 -64
  105. package/template/workflows/ops/H-computer-hygiene/references/22-repositories.md +0 -35
  106. package/template/workflows/ops/H-computer-hygiene/references/30-software-removal.md +0 -41
  107. package/template/workflows/ops/H-computer-hygiene/references/40-report-spec.md +0 -54
  108. package/template/workflows/ops/H-computer-hygiene/references/50-speculo-integration.md +0 -26
  109. package/template/workflows/ops/H-computer-hygiene/references/source-register.json +0 -406
  110. package/template/workflows/ops/H-computer-hygiene/rules/catalog.json +0 -191
  111. package/template/workflows/ops/H-computer-hygiene/scripts/hygiene.py +0 -1348
  112. package/template/workflows/ops/H-computer-hygiene/scripts/run.ps1 +0 -7
  113. package/template/workflows/ops/H-computer-hygiene/scripts/run.sh +0 -5
  114. package/template/workflows/ops/H-computer-hygiene/templates/config.example.json +0 -13
  115. package/template/workflows/ops/H-computer-hygiene/templates/maven-settings.fragment.xml +0 -6
  116. package/template/workflows/ops/H-computer-hygiene/templates/native-evidence.example.json +0 -19
  117. package/template/workflows/ops/H-computer-hygiene/tests/cli_smoke.py +0 -65
  118. package/template/workflows/ops/H-computer-hygiene/tests/make_examples.py +0 -114
  119. package/template/workflows/ops/H-computer-hygiene/tests/test_hygiene.py +0 -486
  120. package/template/workflows/ops/I-intake-and-assess/I-intake-and-assess.md +0 -73
  121. package/template/workflows/ops/I-intake-and-assess/change-status-template.json +0 -26
  122. package/template/workflows/ops/I-intake-and-assess/collector-catalog.md +0 -28
  123. package/template/workflows/ops/I-intake-and-assess/deployment-dossier-template.md +0 -62
  124. package/template/workflows/ops/I-intake-and-assess/global-change-status-template.json +0 -26
  125. package/template/workflows/ops/I-intake-and-assess/project-detection.md +0 -38
  126. package/template/workflows/ops/I-intake-and-assess/request-template.md +0 -44
  127. package/template/workflows/ops/I-intake-and-assess/system-report-template.md +0 -34
  128. package/template/workflows/ops/I-intake-and-assess/target-profile-template.json +0 -24
  129. package/template/workflows/ops/P-plan-and-approve/P-plan-and-approve.md +0 -72
  130. package/template/workflows/ops/P-plan-and-approve/plan-review-template.md +0 -78
  131. package/template/workflows/ops/_state/archive/.gitkeep +0 -1
  132. package/template/workflows/ops/_state/changes/.gitkeep +0 -1
  133. package/template/workflows/ops/common/rules/artifact-contract.md +0 -38
  134. package/template/workflows/ops/common/rules/closure-and-learning.md +0 -27
  135. package/template/workflows/ops/common/rules/evidence-and-redaction.md +0 -22
  136. package/template/workflows/ops/common/rules/execution-loop.md +0 -27
  137. package/template/workflows/ops/common/rules/path-and-scope-contract.md +0 -21
  138. package/template/workflows/ops/common/rules/plan-and-approval.md +0 -25
  139. package/template/workflows/ops/common/rules/project-and-change-scope.md +0 -20
  140. package/template/workflows/ops/common/rules/target-profile-and-release-gates.md +0 -44
  141. package/template/workflows/ops/common/schemas/attempt.schema.json +0 -42
  142. package/template/workflows/ops/common/schemas/change-status.schema.json +0 -43
  143. package/template/workflows/ops/common/schemas/deployment-model.schema.json +0 -26
  144. package/template/workflows/ops/common/schemas/implementation-plan.schema.json +0 -221
  145. package/template/workflows/ops/common/schemas/inventory-snapshot.schema.json +0 -31
  146. package/template/workflows/ops/common/schemas/journal-event.schema.json +0 -22
  147. package/template/workflows/ops/common/schemas/promotion-approval.schema.json +0 -20
  148. package/template/workflows/ops/common/schemas/promotion-manifest.schema.json +0 -22
  149. package/template/workflows/ops/common/schemas/target-profile.schema.json +0 -32
  150. package/template/workflows/ops/common/schemas/verification-state.schema.json +0 -30
  151. package/template/workflows/ops/common/tools/close-change.mjs +0 -177
@@ -0,0 +1,752 @@
1
+ /** OPS safety/contract tests. Adapter simulations are explicitly separate from real local subprocess tests. */
2
+ import assert from "node:assert/strict";
3
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync, readFileSync, chmodSync, existsSync, symlinkSync, readdirSync, statSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { join, dirname } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import test from "node:test";
8
+ import * as core from "../tools/opslib/core.mjs";
9
+ import * as model from "../tools/opslib/model.mjs";
10
+ import * as planner from "../tools/opslib/planner.mjs";
11
+ import * as execution from "../tools/opslib/execution.mjs";
12
+ import * as agent from "../tools/opslib/agent.mjs";
13
+ import * as transport from "../tools/opslib/transport.mjs";
14
+ import * as docs from "../tools/opslib/docs.mjs";
15
+ import * as services from "../tools/opslib/services.mjs";
16
+ import { initialize, analyze, cliHooks } from "../tools/opslib/cli.mjs";
17
+ import { taskFiles } from "../tools/opslib/native_windows.mjs";
18
+
19
+ const { OpsError, UnknownResult, digest, writeJson, readJson, withLock, relative, identifier, rootPath, targetJoin, emptyStatus, now, resolveSecrets, redact, atomicWrite, canonical } = core;
20
+ const PASSWORD = 'TEST-ONLY-$literal-quote"-slash\\-not-production';
21
+
22
+ function deployment(project = "app-a", did = "app-a-prod", root = "/srv/ops", method = "native", host = null) {
23
+ host = host || { platform: "linux", root };
24
+ const depRoot = targetJoin(host, project);
25
+ return {
26
+ deployment_id: did, project_id: project, host_id: "node-a", environment: "prod", instance: "main", layout: "flat",
27
+ method, version: "v1", observed_version: "v1", root: depRoot, status: "completed",
28
+ installed_at: "2026-01-01T00:00:00Z", updated_at: "2026-01-01T00:00:00Z", run_id: "run-old",
29
+ compose_name: method === "compose" ? "ops-" + did : null, storage: [], commands: { start: [], stop: [], verify: [] },
30
+ credential_refs: [], backup: "A project-specific backup is required.", recovery: "No automatic shared-database restore.",
31
+ notes: [], source: { type: "local", location: "/source/" + project, revision: "v1" }, service: {},
32
+ };
33
+ }
34
+
35
+ function minimalInventory(host, request) {
36
+ const snapshots = {};
37
+ for (const p of request.paths || []) {
38
+ snapshots[p] = (request.deep_paths || []).includes(p) ? agent.treeState(p) : agent.fileState(p);
39
+ }
40
+ return {
41
+ identity: host.identity, observed_at: now(), platform: host.platform, hostname: "fixture", architecture: "fixture",
42
+ account: "fixture", uid: null, node: process.execPath, tools: {}, defaults: {},
43
+ diagnostics: { memory: {}, issues: [], disks: {} },
44
+ docker_control: {
45
+ status: "observed", context: "fixture-context", endpoint: "unix:///fixture/docker.sock",
46
+ id: "fixture-docker-id", data_root: targetJoin(host, "_runtime/docker"), compose_version: "2.30.0",
47
+ },
48
+ snapshots,
49
+ };
50
+ }
51
+
52
+ function setupFixture() {
53
+ const tmp = mkdtempSync(join(tmpdir(), "ops-tests-"));
54
+ const state = join(tmp, "controller");
55
+ const target = join(tmp, "server");
56
+ const prevProbe = cliHooks.probeLocal;
57
+ cliHooks.probeLocal = () => ({ identity: agent.fingerprint(), scope: "mock-initial-inventory-only" });
58
+ initialize(state, "control-a");
59
+ cliHooks.probeLocal = prevProbe;
60
+ const platform = process.platform === "win32" ? "windows" : "linux";
61
+ const host = {
62
+ host_id: "node-a", display_name: "Node A", platform, transport: "local",
63
+ root: target, identity: agent.fingerprint(), connection: {},
64
+ };
65
+ const project = {
66
+ project_id: "app-a", display_name: "APP A", kind: "app", service_type: null,
67
+ source: { type: "local", location: join(tmp, "source"), revision: "v1" },
68
+ };
69
+ model.register(state, { hosts: [host], projects: [project] });
70
+ model.putCredential(state, { credential_id: "app-auth", version: 1, purpose: "Synthetic test only", values: { username: "app-user", password: PASSWORD } });
71
+ const spec = (version = "v1", operation = "deploy", program = null) => {
72
+ if (program == null) {
73
+ program = `import { mkdirSync, existsSync, readFileSync, writeFileSync } from "node:fs";
74
+ import { join } from "node:path";
75
+ const dir = join(process.env.OPS_DATA_ROOT, "app", "storage");
76
+ mkdirSync(dir, { recursive: true });
77
+ const p = join(dir, "value.json");
78
+ const n = existsSync(p) ? JSON.parse(readFileSync(p, "utf8")).count + 1 : 1;
79
+ if (process.env.APP_USERNAME !== "app-user") throw new Error("username");
80
+ if (!process.env.APP_PASSWORD.startsWith("TEST-ONLY-")) throw new Error("password");
81
+ writeFileSync(p, JSON.stringify({ count: n, version: ${JSON.stringify(version)} }));
82
+ `;
83
+ }
84
+ return {
85
+ schema_version: 1, worker: "D", operation, reason: "Disposable isolated test only",
86
+ rollback_note: "Retain old data; use a new explicitly approved recovery plan.",
87
+ deployments: [{
88
+ deployment_id: "app-a-prod", project_id: "app-a", host_id: "node-a", environment: "prod", instance: "main",
89
+ layout: "flat", method: "native", version,
90
+ files: [{ path: "artifact/main.mjs", content: program }],
91
+ env: { "app.env": { APP_USERNAME: "{{credential:app-auth@1:username}}", APP_PASSWORD: "{{credential:app-auth@1:password}}" } },
92
+ native: { supervisor: "oneshot", argv: [process.execPath, "{{artifact}}/main.mjs"] },
93
+ credential_refs: ["app-auth@1"],
94
+ health: [{ type: "file", path: "data/app/storage/value.json" }],
95
+ backup: "Back up only after process exit; this fixture has no shared data.",
96
+ recovery: "A new matching-version plan preserves and verifies data.",
97
+ }],
98
+ };
99
+ };
100
+ const plan = (s = null) => {
101
+ const f = join(tmp, "input.json");
102
+ writeJson(f, s || spec());
103
+ const prev = planner.plannerHooks.call;
104
+ planner.plannerHooks.call = (h, request) => minimalInventory(h, request);
105
+ try {
106
+ const result = planner.compilePlan(state, f);
107
+ return [result, readJson(result.plan_path)];
108
+ } finally { planner.plannerHooks.call = prev; }
109
+ };
110
+ const approve = (info) => execution.approval(state, info.run_id, info.plan_digest, "test-admin", "Approve only the exact isolated test plan and its fixture data.");
111
+ const runSpec = (s = null) => {
112
+ const [info, p] = plan(s);
113
+ approve(info);
114
+ const result = execution.apply(state, info.run_id);
115
+ return [info, p, result];
116
+ };
117
+ const graph = () => {
118
+ const s = model.load(state);
119
+ s.deployments["app-a-prod"] = deployment("app-a", "app-a-prod", host.root, "native", host);
120
+ s.deployments["app-a-prod"].credential_refs = ["app-auth@1"];
121
+ s.projects["mysql-main"] = {
122
+ project_id: "mysql-main", display_name: "MySQL", kind: "shared-service", service_type: "mysql",
123
+ source: { type: "image", location: "mysql@sha256:" + "a".repeat(64), revision: "digest-pinned" },
124
+ };
125
+ const d = deployment("mysql-main", "mysql-prod", host.root, "compose", host);
126
+ d.credential_refs = ["mysql-admin@1"];
127
+ d.service = { compose_service: "mysql" };
128
+ s.deployments["mysql-prod"] = d;
129
+ s.allocations["app-db"] = {
130
+ allocation_id: "app-db", provider_deployment_id: "mysql-prod", owner_project_id: "app-a", environment: "prod",
131
+ data_group: "app-a-prod", resource_kind: "database", resource_name: "app_a_prod", credential_ref: "app-auth@1",
132
+ shared_owners: [], status: "active", recovery_scope: "logical", notes: "Fixture logical resource",
133
+ };
134
+ s.bindings["app-db-binding"] = {
135
+ binding_id: "app-db-binding", consumer_deployment_id: "app-a-prod", mode: "shared",
136
+ provider_deployment_id: "mysql-prod", allocation_id: "app-db", endpoint: "mysql-main:3306",
137
+ credential_ref: "app-auth@1", status: "active", component: "mysql", network: "explicit-intranet",
138
+ };
139
+ return s;
140
+ };
141
+ return { tmp, state, target, platform, host, project, spec, plan, approve, runSpec, graph, cleanup: () => { try { rmSync(tmp, { recursive: true, force: true }); } catch {} } };
142
+ }
143
+
144
+ function runFx(fn) {
145
+ const fx = setupFixture();
146
+ try { return fn(fx); } finally { fx.cleanup(); }
147
+ }
148
+
149
+ test("canonical order", () => {
150
+ assert.equal(digest({ b: 1, a: 2 }), digest({ a: 2, b: 1 }));
151
+ });
152
+ test("nonfinite rejected", () => {
153
+ assert.throws(() => canonical(Number.NaN));
154
+ });
155
+ test("duplicate json rejected", () => {
156
+ const d = mkdtempSync(join(tmpdir(), "ops-dup-"));
157
+ const p = join(d, "x.json");
158
+ writeFileSync(p, '{"x":1,"x":2}');
159
+ assert.throws(() => readJson(p), OpsError);
160
+ rmSync(d, { recursive: true, force: true });
161
+ });
162
+ test("exclusive artifact", () => {
163
+ const d = mkdtempSync(join(tmpdir(), "ops-ex-"));
164
+ const p = join(d, "a.json");
165
+ writeJson(p, { x: 1 }, { exclusive: true });
166
+ assert.throws(() => writeJson(p, { x: 2 }, { exclusive: true }), OpsError);
167
+ assert.deepEqual(readJson(p), { x: 1 });
168
+ rmSync(d, { recursive: true, force: true });
169
+ });
170
+ test("lock is not autobroken", () => {
171
+ const d = mkdtempSync(join(tmpdir(), "ops-lock-"));
172
+ withLock(join(d, "lock"), { why: "test" }, () => {
173
+ assert.throws(() => withLock(join(d, "lock"), {}, () => {}), OpsError);
174
+ });
175
+ rmSync(d, { recursive: true, force: true });
176
+ });
177
+ test("symlink rejected", () => {
178
+ const d = mkdtempSync(join(tmpdir(), "ops-link-"));
179
+ mkdirSync(join(d, "real"));
180
+ try { symlinkSync(join(d, "real"), join(d, "link"), "dir"); }
181
+ catch (e) {
182
+ if (process.platform === "win32") { rmSync(d, { recursive: true, force: true }); return; }
183
+ throw e;
184
+ }
185
+ assert.throws(() => atomicWrite(join(d, "link", "a.txt"), "x"), OpsError);
186
+ rmSync(d, { recursive: true, force: true });
187
+ });
188
+ test("file permissions", () => {
189
+ const d = mkdtempSync(join(tmpdir(), "ops-perm-"));
190
+ const p = join(d, "secret");
191
+ atomicWrite(p, PASSWORD);
192
+ if (process.platform !== "win32") {
193
+ assert.equal(statSync(p).mode & 0o777, 0o600);
194
+ }
195
+ rmSync(d, { recursive: true, force: true });
196
+ });
197
+
198
+ test("credential raw values", () => {
199
+ const ledger = { entries: { x: { 1: { values: { password: PASSWORD } } } } };
200
+ assert.equal(resolveSecrets("{{credential:x@1:password}}", ledger), PASSWORD);
201
+ });
202
+ test("credential missing", () => {
203
+ assert.throws(() => resolveSecrets("{{credential:x@1:password}}", { entries: {} }), OpsError);
204
+ });
205
+ test("redaction", () => {
206
+ assert.ok(!redact("pw " + PASSWORD, [PASSWORD]).includes(PASSWORD));
207
+ });
208
+ test("env dollar literal", () => {
209
+ assert.equal(planner.envFile({ P: 'a$$"z' }), 'P=a$$"z\n');
210
+ });
211
+ test("env newline rejected", () => {
212
+ assert.throws(() => planner.envFile({ P: "a\nb" }), OpsError);
213
+ });
214
+ test("env key rejected", () => {
215
+ assert.throws(() => planner.envFile({ "P-INVALID": "a" }), OpsError);
216
+ });
217
+ test("systemd env quote", () => {
218
+ assert.equal(planner.envFile({ P: 'a"\\b' }, { systemd: true }), 'P="a\\"\\\\b"\n');
219
+ });
220
+ test("markdown secret backticks", () => {
221
+ const l = { entries: { x: { 1: { purpose: "test", values: { password: 'a```b|c$"' } } } } };
222
+ assert.ok(docs.credentialsText(["x@1"], l).includes('a```b|c$"'));
223
+ });
224
+ test("windows native json secret", () => {
225
+ const vals = taskFiles("app-a", "C:\\Ops\\app-a", ["python", "C:\\Ops\\app-a\\main.py"], { P: "{{credential:x@1:password}}" }, { account: "X\\User" });
226
+ const original = { kind: "write", json_values: JSON.parse(vals["service/task.json"]), content: vals["service/task.json"] };
227
+ const actual = execution.resolvedOp(original, { entries: { x: { 1: { values: { password: PASSWORD } } } } });
228
+ assert.equal(JSON.parse(Buffer.from(actual.content_b64, "base64").toString("utf8")).environment.P, PASSWORD);
229
+ });
230
+
231
+ for (const [index, value] of ["", "../a", "a/../b", "a//b", "/a", "a\\b", "C:a", "a/", "./a", "a\x00b"].entries()) {
232
+ test(`relative reject ${String(index).padStart(2, "0")}`, () => {
233
+ assert.throws(() => relative(value), OpsError);
234
+ });
235
+ }
236
+ for (const [index, value] of ["app-a", "mysql-main", "a1", "host-22"].entries()) {
237
+ test(`identifier valid ${String(index).padStart(2, "0")}`, () => {
238
+ assert.equal(identifier(value), value);
239
+ });
240
+ }
241
+ for (const [index, value] of ["", "App", "a_b", "-a", "a-", "a/b", "con", "com1", "lpt9", "a".repeat(81)].entries()) {
242
+ test(`identifier reject ${String(index).padStart(2, "0")}`, () => {
243
+ assert.throws(() => identifier(value), OpsError);
244
+ });
245
+ }
246
+ for (const [index, value] of ["/", "/etc", "/var", "/tmp", "/srv", "relative", "/srv/../etc"].entries()) {
247
+ test(`posix root reject ${String(index).padStart(2, "0")}`, () => {
248
+ assert.throws(() => rootPath(value, "linux"), OpsError);
249
+ });
250
+ }
251
+ for (const [index, value] of ["C:\\", "C:\\Windows", "C:\\Users", "\\\\server\\share\\ops", "C:\\Ops\\..\\data", "C:\\Ops:stream"].entries()) {
252
+ test(`windows root reject ${String(index).padStart(2, "0")}`, () => {
253
+ assert.throws(() => rootPath(value, "windows"), OpsError);
254
+ });
255
+ }
256
+
257
+ test("empty v3 valid", () => { model.validateStatus(emptyStatus()); });
258
+
259
+ test("resource contracts", async (t) => {
260
+ const each = (name, fn) => t.test(name, () => runFx(fn));
261
+ await each("unknown schema field", (fx) => {
262
+ const s = model.load(fx.state); s.secret = "bad";
263
+ assert.throws(() => model.validateStatus(s), OpsError);
264
+ });
265
+ await each("legacy blocked", (fx) => {
266
+ assert.throws(() => model.validateStatus({ schema_version: 2, active: [], archived: [] }), OpsError);
267
+ });
268
+ await each("credential version immutable", (fx) => {
269
+ assert.throws(() => model.putCredential(fx.state, { credential_id: "app-auth", version: 1, purpose: "x", values: { username: "u", password: "changed" } }), OpsError);
270
+ });
271
+ await each("credential permissions rejected", (fx) => {
272
+ if (process.platform === "win32") return;
273
+ chmodSync(join(fx.state, "private", "credentials.json"), 0o644);
274
+ assert.throws(() => model.ledgerLoad(fx.state), OpsError);
275
+ chmodSync(join(fx.state, "private", "credentials.json"), 0o600);
276
+ });
277
+ await each("valid shared graph", (fx) => { model.validateStatus(fx.graph()); });
278
+ await each("duplicate physical host", (fx) => {
279
+ const s = model.load(fx.state); s.hosts["node-b"] = { ...fx.host, host_id: "node-b" };
280
+ assert.throws(() => model.validateStatus(s), OpsError);
281
+ });
282
+ await each("host root nested", (fx) => {
283
+ const s = model.load(fx.state); s.hosts["node-b"] = { ...fx.host, host_id: "node-b", root: join(fx.target, "nested") };
284
+ assert.throws(() => model.validateStatus(s), OpsError);
285
+ });
286
+ await each("identity platform mismatch", (fx) => {
287
+ const other = fx.platform === "windows" ? "linux" : "windows";
288
+ const otherRoot = other === "linux" ? "/srv/ops" : "C:\\Ops";
289
+ const s = model.load(fx.state); s.hosts["node-b"] = { ...fx.host, host_id: "node-b", platform: other, root: otherRoot };
290
+ assert.throws(() => model.validateStatus(s), OpsError);
291
+ });
292
+ await each("dedicated cannot claim shared provider", (fx) => {
293
+ const s = fx.graph(); Object.assign(s.bindings["app-db-binding"], { mode: "dedicated", allocation_id: null });
294
+ assert.throws(() => model.validateStatus(s), OpsError);
295
+ });
296
+ await each("project root cannot override", (fx) => {
297
+ const s = fx.graph(); s.deployments["app-a-prod"].root = join(fx.target, "elsewhere");
298
+ assert.throws(() => model.validateStatus(s), OpsError);
299
+ });
300
+ await each("native data outside root", (fx) => {
301
+ const s = fx.graph(); s.deployments["app-a-prod"].storage = [{ component: "app", purpose: "data", path: "/tmp/data" }];
302
+ assert.throws(() => model.validateStatus(s), OpsError);
303
+ });
304
+ await each("flat instances overlap", (fx) => {
305
+ const s = fx.graph();
306
+ const d = structuredClone(s.deployments["app-a-prod"]);
307
+ Object.assign(d, { deployment_id: "app-a-test", layout: "instances", environment: "test", root: join(fx.target, "app-a", "instances", "test", "main") });
308
+ s.deployments[d.deployment_id] = d;
309
+ assert.throws(() => model.validateStatus(s), OpsError);
310
+ });
311
+ await each("orphan provider", (fx) => {
312
+ const s = fx.graph(); s.allocations["app-db"].provider_deployment_id = "missing";
313
+ assert.throws(() => model.validateStatus(s), OpsError);
314
+ });
315
+ await each("duplicate allocation", (fx) => {
316
+ const s = fx.graph(); s.allocations["app-db-copy"] = { ...s.allocations["app-db"], allocation_id: "app-db-copy" };
317
+ assert.throws(() => model.validateStatus(s), OpsError);
318
+ });
319
+ await each("wrong binding password version", (fx) => {
320
+ const s = fx.graph(); s.bindings["app-db-binding"].credential_ref = "app-auth@2";
321
+ assert.throws(() => model.validateStatus(s), OpsError);
322
+ });
323
+ await each("external cannot claim managed data", (fx) => {
324
+ const s = fx.graph(); s.bindings["app-db-binding"].mode = "external";
325
+ assert.throws(() => model.validateStatus(s), OpsError);
326
+ });
327
+ await each("retired provider allocation", (fx) => {
328
+ const s = fx.graph(); s.allocations["app-db"].status = "retired";
329
+ assert.throws(() => model.validateStatus(s), OpsError);
330
+ });
331
+ await each("cross environment not implicit", (fx) => {
332
+ const s = fx.graph(); s.allocations["app-db"].environment = "test";
333
+ assert.throws(() => model.validateStatus(s), OpsError);
334
+ });
335
+ await each("unknown project kind", (fx) => {
336
+ const s = fx.graph(); s.projects["mysql-main"].kind = "other";
337
+ assert.throws(() => model.validateStatus(s), OpsError);
338
+ });
339
+ await each("strict docker cannot be disabled", (fx) => {
340
+ const s = fx.graph(); s.policies.strict_docker_root = false;
341
+ assert.throws(() => model.validateStatus(s), OpsError);
342
+ });
343
+ await each("provider admin app separation", (fx) => {
344
+ const s = fx.graph();
345
+ assert.throws(() => services.allocationOperation(s, s.deployments["mysql-prod"], s.allocations["app-db"], { adapter: "mysql", admin_credential_ref: "app-auth@1", app_username: "app-user" }), OpsError);
346
+ });
347
+ await each("mysql no global admin grants", (fx) => {
348
+ const s = fx.graph();
349
+ assert.throws(() => services.allocationOperation(s, s.deployments["mysql-prod"], s.allocations["app-db"], { adapter: "mysql", admin_credential_ref: "mysql-admin@1", app_username: "app-user", privileges: ["SUPER"] }), OpsError);
350
+ });
351
+ await each("mysql explicit scoped grants", (fx) => {
352
+ const s = fx.graph();
353
+ const op = services.allocationOperation(s, s.deployments["mysql-prod"], s.allocations["app-db"], { adapter: "mysql", admin_credential_ref: "mysql-admin@1", app_username: "app-user", privileges: ["SELECT"] });
354
+ assert.deepEqual(op.privileges, ["SELECT"]);
355
+ assert.equal(op.kind, "mysql-allocation");
356
+ });
357
+ await each("source docs preserve deployment revision", (fx) => {
358
+ const s = fx.graph();
359
+ s.projects["app-a"].source.revision = "newer-project-source";
360
+ const text = docs.deploymentReadme(s, s.deployments["app-a-prod"], "run-test", now());
361
+ assert.ok(!text.includes("newer-project-source"));
362
+ assert.ok(text.includes("v1"));
363
+ });
364
+ });
365
+
366
+ test("planner contracts", async (t) => {
367
+ const each = (name, fn) => t.test(name, () => runFx(fn));
368
+ await each("plan writes no target", (fx) => {
369
+ const [info, plan] = fx.plan();
370
+ assert.equal(existsSync(fx.target), false);
371
+ assert.ok(!readFileSync(info.report, "utf8").includes(PASSWORD));
372
+ assert.equal(plan.registry_digest, digest(model.load(fx.state)));
373
+ });
374
+ await each("missing health rejected", (fx) => {
375
+ const s = fx.spec(); s.deployments[0].health = [];
376
+ assert.throws(() => fx.plan(s), OpsError);
377
+ });
378
+ await each("unknown spec field rejected", (fx) => {
379
+ const s = fx.spec(); s.do_anything = true;
380
+ assert.throws(() => fx.plan(s), OpsError);
381
+ });
382
+ await each("inline native code rejected", (fx) => {
383
+ const s = fx.spec(); s.deployments[0].native.argv = [process.execPath, "-c", "print(1)"];
384
+ assert.throws(() => fx.plan(s), OpsError);
385
+ });
386
+ await each("native outside arg rejected", (fx) => {
387
+ const s = fx.spec(); s.deployments[0].native.argv = [process.execPath, "/tmp/other.py"];
388
+ assert.throws(() => fx.plan(s), OpsError);
389
+ });
390
+ await each("env path escape rejected", (fx) => {
391
+ const s = fx.spec(); s.deployments[0].env["app.env"].APP_DATA_DIR = "/tmp/app-data";
392
+ assert.throws(() => fx.plan(s), OpsError);
393
+ });
394
+ await each("env reserved root rejected", (fx) => {
395
+ const s = fx.spec(); s.deployments[0].env["app.env"].OPS_DATA_ROOT = "{{data}}";
396
+ assert.throws(() => fx.plan(s), OpsError);
397
+ });
398
+ await each("native multiple env requires order", (fx) => {
399
+ const s = fx.spec(); s.deployments[0].env["extra.env"] = { EXTRA: "1" };
400
+ assert.throws(() => fx.plan(s), OpsError);
401
+ });
402
+ await each("unknown env file rejected", (fx) => {
403
+ const s = fx.spec(); s.deployments[0].native.env_files = ["missing.env"];
404
+ assert.throws(() => fx.plan(s), OpsError);
405
+ });
406
+ await each("env vars reach native command", (fx) => {
407
+ const [, p] = fx.plan();
408
+ const commands = p.operations.filter((x) => x.kind === "command");
409
+ assert.ok("APP_PASSWORD" in commands[0].env);
410
+ assert.ok(commands[0].env.HOME.startsWith(join(fx.target, "app-a")));
411
+ });
412
+ await each("host command requires writes", (fx) => {
413
+ assert.throws(() => fx.plan({ schema_version: 1, worker: "H", operation: "prepare", reason: "test", rollback_note: "test", host_actions: [{ host_id: "node-a", kind: "command", reason: "test", argv: ["echo", "safe"] }] }), OpsError);
414
+ });
415
+ await each("host command requires verification", (fx) => {
416
+ assert.throws(() => fx.plan({ schema_version: 1, worker: "H", operation: "prepare", reason: "test", rollback_note: "test", host_actions: [{ host_id: "node-a", kind: "command", reason: "test", argv: ["echo", "safe"], writes: [join(fx.target, "_host", "x")] }] }), OpsError);
417
+ });
418
+ await each("project file traversal", (fx) => {
419
+ const s = fx.spec(); s.deployments[0].files[0].path = "../else.py";
420
+ assert.throws(() => fx.plan(s), OpsError);
421
+ });
422
+ await each("plaintext readme cannot be arbitrary file", (fx) => {
423
+ const s = fx.spec(); s.deployments[0].files.push({ path: "README.md", content: "fake success" });
424
+ assert.throws(() => fx.plan(s), OpsError);
425
+ });
426
+ await each("resource update root fixed", (fx) => {
427
+ const s = fx.spec(); s.resource_updates = { hosts: [{ ...fx.host, root: join(fx.tmp, "else") }] };
428
+ assert.throws(() => fx.plan(s), OpsError);
429
+ });
430
+ await each("resource source update planned", (fx) => {
431
+ const s = fx.spec();
432
+ const p = structuredClone(fx.project);
433
+ p.source.revision = "v2";
434
+ s.resource_updates = { projects: [p] };
435
+ const [, plan] = fx.plan(s);
436
+ assert.equal(plan.registry_after.deployments["app-a-prod"].source.revision, "v2");
437
+ assert.equal(model.load(fx.state).projects["app-a"].source.revision, "v1");
438
+ });
439
+ await each("wrong approval digest", (fx) => {
440
+ const [info] = fx.plan();
441
+ assert.throws(() => execution.approval(fx.state, info.run_id, "0".repeat(64), "test", "explicit statement"), OpsError);
442
+ });
443
+ await each("state drift blocks approval", (fx) => {
444
+ const [info] = fx.plan();
445
+ model.register(fx.state, {});
446
+ assert.throws(() => fx.approve(info), OpsError);
447
+ });
448
+ await each("approval is immutable", (fx) => {
449
+ const [info] = fx.plan();
450
+ fx.approve(info);
451
+ assert.throws(() => fx.approve(info), OpsError);
452
+ });
453
+ await each("plan tamper blocks apply", (fx) => {
454
+ const [info, p] = fx.plan();
455
+ fx.approve(info);
456
+ p.reason = "changed";
457
+ writeJson(info.plan_path, p);
458
+ assert.throws(() => execution.apply(fx.state, info.run_id), OpsError);
459
+ });
460
+ await each("engine drift blocks apply", (fx) => {
461
+ const [info] = fx.plan();
462
+ fx.approve(info);
463
+ const prev = execution.executionHooks.engineDigest;
464
+ execution.executionHooks.engineDigest = () => "0".repeat(64);
465
+ try { assert.throws(() => execution.apply(fx.state, info.run_id), OpsError); }
466
+ finally { execution.executionHooks.engineDigest = prev; }
467
+ });
468
+ await each("credential drift blocks apply", (fx) => {
469
+ const [info] = fx.plan();
470
+ fx.approve(info);
471
+ const l = model.ledgerLoad(fx.state);
472
+ l.entries["app-auth"]["1"].values.password = "changed";
473
+ writeJson(join(fx.state, "private", "credentials.json"), l);
474
+ assert.throws(() => execution.apply(fx.state, info.run_id), OpsError);
475
+ });
476
+ await each("existing unowned project not overwritten", (fx) => {
477
+ mkdirSync(join(fx.target, "app-a"), { recursive: true });
478
+ assert.throws(() => fx.plan(), OpsError);
479
+ });
480
+ await each("compose requires digest", (fx) => {
481
+ assert.throws(() => planner.composeModel({ compose: { services: { app: { image: "example:latest" } } } }, fx.host, join(fx.target, "app-a"), "ops-app-a", {}, {}), OpsError);
482
+ });
483
+ await each("compose rejects named volume", (fx) => {
484
+ assert.throws(() => planner.composeModel({ compose: { services: { app: { image: "example@sha256:" + "a".repeat(64), volumes: [{ type: "volume", source: "named", target: "/data" }] } } } }, fx.host, join(fx.target, "app-a"), "ops-app-a", {}, {}), OpsError);
485
+ });
486
+ await each("compose rejects cross app data", (fx) => {
487
+ assert.throws(() => planner.composeModel({ compose: { services: { app: { image: "example@sha256:" + "a".repeat(64), volumes: [{ type: "bind", source: join(fx.target, "app-b", "data", "a", "b"), target: "/data" }] } } } }, fx.host, join(fx.target, "app-a"), "ops-app-a", {}, {}), OpsError);
488
+ });
489
+ await each("compose rejects writable root", (fx) => {
490
+ assert.throws(() => planner.composeModel({ compose: { services: { app: { image: "example@sha256:" + "a".repeat(64), read_only: false } } } }, fx.host, join(fx.target, "app-a"), "ops-app-a", {}, {}), OpsError);
491
+ });
492
+ await each("compose safe persistence model", (fx) => {
493
+ const [value] = planner.composeModel({ compose: { services: { app: { image: "example@sha256:" + "a".repeat(64), volumes: [{ type: "bind", source: "data/app/storage", target: "/data" }] } } } }, fx.host, join(fx.target, "app-a"), "ops-app-a", { "app.env": {} }, {});
494
+ assert.equal(value.services.app.read_only, true);
495
+ assert.equal(value.services.app.volumes[0].bind.create_host_path, false);
496
+ assert.equal(value.services.app.env_file[0].format, "raw");
497
+ });
498
+ });
499
+
500
+ test("real native and dual docs", () => runFx((fx) => {
501
+ const [info, , result] = fx.runSpec();
502
+ assert.equal(result.status, "completed", JSON.stringify(result));
503
+ const dep = model.load(fx.state).deployments["app-a-prod"];
504
+ assert.equal(dep.observed_version, "v1");
505
+ const remote = join(fx.target, "app-a");
506
+ const local = join(fx.state, "hosts", "node-a", "deployments", "app-a-prod");
507
+ assert.ok(!readFileSync(join(remote, "README.md"), "utf8").includes(PASSWORD));
508
+ assert.ok(readFileSync(join(remote, "OPERATIONS.md"), "utf8").includes(PASSWORD));
509
+ assert.ok(readFileSync(join(local, "README.md"), "utf8").includes(PASSWORD));
510
+ assert.equal(readJson(join(local, "docs-receipt.json")).status, "both-sides-verified");
511
+ assert.deepEqual(readFileSync(join(remote, "env", "app.env")), readFileSync(join(local, "server-files", "env", "app.env")));
512
+ assert.equal(readJson(join(remote, "data", "app", "storage", "value.json")).count, 1);
513
+ for (const f of [join(dirname(info.plan_path), "journal.jsonl"), join(dirname(info.plan_path), "execution.json")]) {
514
+ assert.ok(!readFileSync(f, "utf8").includes(PASSWORD));
515
+ }
516
+ }));
517
+ test("upgrade and rollback preserve data", () => runFx((fx) => {
518
+ let r;
519
+ [, , r] = fx.runSpec();
520
+ assert.equal(r.status, "completed");
521
+ [, , r] = fx.runSpec(fx.spec("v2", "upgrade"));
522
+ assert.equal(r.status, "completed", JSON.stringify(r));
523
+ [, , r] = fx.runSpec(fx.spec("v1", "rollback"));
524
+ assert.equal(r.status, "completed", JSON.stringify(r));
525
+ const value = readJson(join(fx.target, "app-a", "data", "app", "storage", "value.json"));
526
+ assert.deepEqual(value, { count: 3, version: "v1" });
527
+ assert.equal(readdirSync(join(fx.target, "app-a", "releases")).length, 3);
528
+ }));
529
+ test("retirement preserves data and credentials", () => runFx((fx) => {
530
+ let r, p;
531
+ [, , r] = fx.runSpec();
532
+ assert.equal(r.status, "completed");
533
+ [, p, r] = fx.runSpec({ schema_version: 1, worker: "D", operation: "uninstall", reason: "Stop only the disposable app", retire_deployments: ["app-a-prod"], rollback_note: "All data and credentials retained." });
534
+ assert.equal(r.status, "completed", JSON.stringify(r));
535
+ assert.equal(model.load(fx.state).deployments["app-a-prod"].status, "retired");
536
+ assert.equal(existsSync(join(fx.target, "app-a", "data", "app", "storage", "value.json")), true);
537
+ assert.equal(existsSync(join(fx.target, "app-a", "env", "app.env")), true);
538
+ assert.equal(p.operations.some((o) => ["quarantine", "purge-quarantine"].includes(o.kind)), false);
539
+ }));
540
+ test("second apply blocked resume completed no repeat", () => runFx((fx) => {
541
+ const [info, , r] = fx.runSpec();
542
+ assert.equal(r.status, "completed");
543
+ assert.throws(() => execution.apply(fx.state, info.run_id), OpsError);
544
+ const again = execution.apply(fx.state, info.run_id, { resume: true });
545
+ assert.equal(again.unchanged, true);
546
+ assert.equal(readJson(join(fx.target, "app-a", "data", "app", "storage", "value.json")).count, 1);
547
+ }));
548
+ test("docs retry does not repeat native command", () => runFx((fx) => {
549
+ const [info] = fx.plan();
550
+ fx.approve(info);
551
+ const real = execution.executionHooks.call;
552
+ let failed = false;
553
+ execution.executionHooks.call = (host, request, kw) => {
554
+ if (request.action === "step" && String(request.operation?.step_id || "").startsWith("docs-") && !failed) {
555
+ failed = true;
556
+ throw new UnknownResult("simulated document transport interruption before dispatch");
557
+ }
558
+ return real(host, request, kw);
559
+ };
560
+ let r;
561
+ try { r = execution.apply(fx.state, info.run_id); }
562
+ finally { execution.executionHooks.call = real; }
563
+ assert.equal(r.status, "docs_pending", JSON.stringify(r));
564
+ assert.equal(existsSync(join(fx.state, "hosts", "node-a", "deployments", "app-a-prod", "README.md")), true);
565
+ r = execution.apply(fx.state, info.run_id, { docsOnly: true });
566
+ assert.equal(r.status, "completed", JSON.stringify(r));
567
+ assert.equal(readJson(join(fx.target, "app-a", "data", "app", "storage", "value.json")).count, 1);
568
+ }));
569
+ test("actual failed command not replayed", () => runFx((fx) => {
570
+ const [info, , r] = fx.runSpec(fx.spec("v1", "deploy", "process.exit(7);\n"));
571
+ assert.equal(r.status, "partial", JSON.stringify(r));
572
+ const dep = model.load(fx.state).deployments["app-a-prod"];
573
+ assert.equal(dep.status, "failed");
574
+ assert.equal(dep.observed_version, null);
575
+ const again = execution.apply(fx.state, info.run_id, { resume: true });
576
+ assert.equal(again.status, "failed");
577
+ assert.ok(JSON.stringify(again).includes("not replayed"));
578
+ }));
579
+ test("unknown identity blocks target before data", () => runFx((fx) => {
580
+ const [info] = fx.plan();
581
+ fx.approve(info);
582
+ const wrong = { ...fx.host, identity: "0".repeat(64) };
583
+ assert.throws(() => transport.call(wrong, { action: "probe" }), OpsError);
584
+ assert.equal(existsSync(fx.target), false);
585
+ }));
586
+ test("remote file drift stops before overwrite", () => runFx((fx) => {
587
+ const [info] = fx.plan();
588
+ fx.approve(info);
589
+ const dest = join(fx.target, "app-a", "env", "app.env");
590
+ mkdirSync(dirname(dest), { recursive: true });
591
+ writeFileSync(dest, "UNAPPROVED=1\n");
592
+ const r = execution.apply(fx.state, info.run_id);
593
+ assert.equal(r.status, "failed");
594
+ assert.equal(readFileSync(dest, "utf8"), "UNAPPROVED=1\n");
595
+ }));
596
+ test("journal tamper detected", () => runFx((fx) => {
597
+ const [info, , r] = fx.runSpec();
598
+ const j = join(dirname(info.plan_path), "journal.jsonl");
599
+ const rows = readFileSync(j, "utf8").split(/\r?\n/).filter(Boolean);
600
+ const value = JSON.parse(rows[0]);
601
+ value.kind = "tampered";
602
+ rows[0] = JSON.stringify(value);
603
+ writeFileSync(j, rows.join("\n") + "\n");
604
+ assert.throws(() => execution.verifyJournal(j), OpsError);
605
+ }));
606
+ test("quarantine then separate purge", () => runFx((fx) => {
607
+ let hspec = {
608
+ schema_version: 1, worker: "H", operation: "maintain", hosts: ["node-a"], reason: "Disposable cache fixture",
609
+ rollback_note: "Keep isolated cache until separate purge is approved.",
610
+ host_actions: [{ host_id: "node-a", kind: "write-file", reason: "Create only fixture cache", path: "_host/cache/demo/cache.txt", content: "Disposable cache" }],
611
+ };
612
+ let r, info;
613
+ [, , r] = fx.runSpec(hspec);
614
+ assert.equal(r.status, "completed", JSON.stringify(r));
615
+ hspec = { ...hspec, host_actions: [{ host_id: "node-a", kind: "quarantine", reason: "Explicit isolate", path: join(fx.target, "_host", "cache", "demo", "cache.txt") }] };
616
+ [info, , r] = fx.runSpec(hspec);
617
+ assert.equal(r.status, "completed", JSON.stringify(r));
618
+ const q = join(fx.target, "_host", "quarantine", info.run_id, "item-0001");
619
+ assert.equal(existsSync(q), true);
620
+ hspec = { ...hspec, host_actions: [{ host_id: "node-a", kind: "purge-quarantine", reason: "Separate irreversible deletion of this one approved cache fixture", path: q }] };
621
+ [, , r] = fx.runSpec(hspec);
622
+ assert.equal(r.status, "completed", JSON.stringify(r));
623
+ assert.equal(existsSync(q), false);
624
+ }));
625
+
626
+ function agentFixture() {
627
+ const tmp = mkdtempSync(join(tmpdir(), "ops-agent-test-"));
628
+ const root = join(tmp, "target");
629
+ const req = { action: "lock", root, identity: agent.fingerprint(), host_id: "node-a", run_id: "run-test", controller_id: "control-a", plan_digest: "a".repeat(64), external_files: [] };
630
+ agent.main(req);
631
+ return {
632
+ tmp, root, req,
633
+ step: (op) => agent.main({ ...req, action: "step", operation: op, operation_digest: digest(op) }),
634
+ cleanup: () => { try { rmSync(tmp, { recursive: true, force: true }); } catch {} },
635
+ };
636
+ }
637
+
638
+ test("same receipt is not reexecuted", () => {
639
+ const fx = agentFixture();
640
+ try {
641
+ const op = { step_id: "step-1", kind: "write", path: join(fx.root, "a.txt"), content_b64: Buffer.from("A").toString("base64"), expected: { kind: "absent" } };
642
+ const one = fx.step(op); const two = fx.step(op);
643
+ assert.deepEqual(one, two);
644
+ } finally { fx.cleanup(); }
645
+ });
646
+ test("started only is unknown", () => {
647
+ const fx = agentFixture();
648
+ try {
649
+ const p = join(fx.root, "_host", "runs", "run-test", "receipts");
650
+ mkdirSync(p, { recursive: true });
651
+ writeFileSync(join(p, "step-1.started.json"), "{}");
652
+ const result = fx.step({ step_id: "step-1", kind: "mkdir", path: join(fx.root, "x") });
653
+ assert.equal(result.status, "unknown");
654
+ assert.equal(existsSync(join(fx.root, "x")), false);
655
+ } finally { fx.cleanup(); }
656
+ });
657
+ test("target lock other owner rejected", () => {
658
+ const fx = agentFixture();
659
+ try { assert.throws(() => agent.main({ ...fx.req, run_id: "run-other" }), agent.Failure); }
660
+ finally { fx.cleanup(); }
661
+ });
662
+ test("write beyond root fails", () => {
663
+ const fx = agentFixture();
664
+ try {
665
+ const result = fx.step({ step_id: "step-1", kind: "write", path: join(dirname(fx.root), "outside"), content_b64: "WA==", expected: { kind: "absent" } });
666
+ assert.equal(result.status, "failed");
667
+ } finally { fx.cleanup(); }
668
+ });
669
+ test("identical bytes still secure permissions", () => {
670
+ const fx = agentFixture();
671
+ try {
672
+ const p = join(fx.root, "secret");
673
+ writeFileSync(p, "X");
674
+ if (process.platform !== "win32") chmodSync(p, 0o644);
675
+ const result = fx.step({ step_id: "step-1", kind: "write", path: p, content_b64: "WA==", expected: agent.fileState(p), mode: 0o600 });
676
+ assert.equal(result.status, "succeeded");
677
+ if (process.platform !== "win32") assert.equal(statSync(p).mode & 0o777, 0o600);
678
+ } finally { fx.cleanup(); }
679
+ });
680
+ test("quarantine data forbidden", () => {
681
+ const fx = agentFixture();
682
+ try {
683
+ const p = join(fx.root, "app-a", "data", "logs", "a");
684
+ mkdirSync(dirname(p), { recursive: true });
685
+ writeFileSync(p, "business");
686
+ const result = fx.step({ step_id: "step-1", kind: "quarantine", path: p, item_id: "item-1", expected: agent.fileState(p) });
687
+ assert.equal(result.status, "failed");
688
+ assert.equal(existsSync(p), true);
689
+ } finally { fx.cleanup(); }
690
+ });
691
+ test("purge needs isolation receipt", () => {
692
+ const fx = agentFixture();
693
+ try {
694
+ const p = join(fx.root, "_host", "quarantine", "run-old", "item-1");
695
+ mkdirSync(dirname(p), { recursive: true });
696
+ writeFileSync(p, "unknown");
697
+ const result = fx.step({ step_id: "step-1", kind: "purge-quarantine", path: p, expected: agent.treeState(p) });
698
+ assert.equal(result.status, "failed");
699
+ } finally { fx.cleanup(); }
700
+ });
701
+ test("mirror requires https", () => {
702
+ assert.throws(() => agent.benchmarkMirrors({ candidates: [{ id: "x", ecosystem: "python", url: "http://example.invalid/a", expected_sha256: "0".repeat(64), trust: "official", approved: true }] }), agent.Failure);
703
+ });
704
+ test("mirror requires approval", () => {
705
+ assert.throws(() => agent.benchmarkMirrors({ candidates: [{ id: "x", ecosystem: "python", url: "https://example.invalid/a", expected_sha256: "0".repeat(64), trust: "official", approved: false }] }), agent.Failure);
706
+ });
707
+ test("analyze does not run repo", () => {
708
+ const fx = agentFixture();
709
+ try {
710
+ const source = join(fx.root, "source");
711
+ mkdirSync(source);
712
+ writeFileSync(join(source, "package.json"), '{"scripts":{"postinstall":"NEVER-RUN"}}');
713
+ writeFileSync(join(source, ".env"), PASSWORD);
714
+ const result = analyze(source);
715
+ assert.deepEqual(result.detected, ["node"]);
716
+ assert.ok(!JSON.stringify(result).includes(PASSWORD));
717
+ assert.equal(result.evidence.length, 1);
718
+ } finally { fx.cleanup(); }
719
+ });
720
+
721
+ test("ssh contract", () => {
722
+ const d = mkdtempSync(join(tmpdir(), "ops-ssh-"));
723
+ const kh = join(d, "known_hosts");
724
+ writeFileSync(kh, "fixture pinned key\n");
725
+ const h = { host_id: "node-a", platform: "linux", transport: "ssh", identity: "a".repeat(64), connection: { hostname: "host.invalid", username: "ops", known_hosts: kh, node: "node" } };
726
+ const captured = {};
727
+ const prev = transport.transportHooks.spawnSync;
728
+ transport.transportHooks.spawnSync = (cmd, args, kw) => {
729
+ captured.argv = [cmd, ...args];
730
+ Object.assign(captured, kw);
731
+ return { status: 0, stdout: '{"ok":true,"result":{"scope":"simulated"}}', stderr: "" };
732
+ };
733
+ try {
734
+ transport.call(h, { action: "step", secrets: [PASSWORD] });
735
+ const joined = captured.argv.join(" ");
736
+ assert.ok(captured.argv.includes("StrictHostKeyChecking=yes"));
737
+ assert.ok(captured.argv.includes("BatchMode=yes"));
738
+ assert.ok(!joined.includes(PASSWORD));
739
+ assert.ok(captured.argv.includes("UserKnownHostsFile=" + kh));
740
+ } finally { transport.transportHooks.spawnSync = prev; rmSync(d, { recursive: true, force: true }); }
741
+ });
742
+
743
+ test("knownhosts digest drift", () => {
744
+ const d = mkdtempSync(join(tmpdir(), "ops-kh-"));
745
+ const kh = join(d, "known_hosts");
746
+ writeFileSync(kh, "first");
747
+ const h = { transport: "ssh", connection: { known_hosts: kh } };
748
+ const before = transport.hostTransportDigest(h);
749
+ writeFileSync(kh, "second");
750
+ assert.notEqual(before, transport.hostTransportDigest(h));
751
+ rmSync(d, { recursive: true, force: true });
752
+ });