@wasm-oj/server 0.2.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 (94) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +6 -0
  3. package/THIRD_PARTY_NOTICES.md +302 -0
  4. package/crates/runtime-core/Cargo.lock +5099 -0
  5. package/crates/runtime-core/Cargo.toml +66 -0
  6. package/crates/runtime-core/README.md +47 -0
  7. package/crates/runtime-core/src/bin/wasm-oj-compiler.rs +418 -0
  8. package/crates/runtime-core/src/bin/wasm-oj-runner.rs +294 -0
  9. package/crates/runtime-core/src/capabilities.rs +118 -0
  10. package/crates/runtime-core/src/compiler.rs +658 -0
  11. package/crates/runtime-core/src/contract.rs +5 -0
  12. package/crates/runtime-core/src/deterministic.rs +1051 -0
  13. package/crates/runtime-core/src/error.rs +58 -0
  14. package/crates/runtime-core/src/filesystem.rs +547 -0
  15. package/crates/runtime-core/src/filesystem_quota.rs +167 -0
  16. package/crates/runtime-core/src/go_compiler_session.rs +297 -0
  17. package/crates/runtime-core/src/interactive.rs +1019 -0
  18. package/crates/runtime-core/src/judge_package.rs +1539 -0
  19. package/crates/runtime-core/src/lib.rs +98 -0
  20. package/crates/runtime-core/src/memory.rs +84 -0
  21. package/crates/runtime-core/src/meter.rs +549 -0
  22. package/crates/runtime-core/src/module_imports.rs +149 -0
  23. package/crates/runtime-core/src/module_policy.rs +714 -0
  24. package/crates/runtime-core/src/output.rs +204 -0
  25. package/crates/runtime-core/src/run/mod.rs +208 -0
  26. package/crates/runtime-core/src/run/native.rs +260 -0
  27. package/crates/runtime-core/src/run/web.rs +229 -0
  28. package/crates/runtime-core/src/run/web_runtime.rs +109 -0
  29. package/crates/runtime-core/src/types.rs +268 -0
  30. package/crates/runtime-core/src/web.rs +83 -0
  31. package/dist/chunks/go-toolchain-Dbt-lp2L.js +426 -0
  32. package/dist/chunks/java-toolchain-DajoRCHu.js +44 -0
  33. package/dist/chunks/python-toolchain-Dx834o2A.js +4 -0
  34. package/dist/chunks/rust-toolchain-CJ3sMxPE.js +252 -0
  35. package/dist/chunks/toolchains-C6KuA1yM.js +224 -0
  36. package/dist/go-stage.mjs +193 -0
  37. package/dist/index.d.ts +221 -0
  38. package/dist/index.js +4393 -0
  39. package/dist/java-stage.mjs +111 -0
  40. package/dist/python-stage.mjs +90 -0
  41. package/dist/rustc-stage.mjs +301 -0
  42. package/dist/server-build-stage.mjs +2564 -0
  43. package/dist/server-runner-stage.mjs +155 -0
  44. package/licenses/fflate-MIT.txt +21 -0
  45. package/licenses/runtime-core-dependencies.html +6253 -0
  46. package/licenses/runtime-core-dependencies.json +3041 -0
  47. package/licenses/wasmer-sdk-MIT.txt +21 -0
  48. package/licenses/wasmer-sdk-dependencies.html +6901 -0
  49. package/licenses/wasmer-sdk-dependencies.json +3013 -0
  50. package/package.json +70 -0
  51. package/rust-toolchain.toml +5 -0
  52. package/testdata/wojjdg02-v2-text.hex +1 -0
  53. package/vendor/shared-buffer/Cargo.toml +22 -0
  54. package/vendor/shared-buffer/LICENSE_APACHE.md +176 -0
  55. package/vendor/shared-buffer/LICENSE_MIT.md +25 -0
  56. package/vendor/shared-buffer/README.md +34 -0
  57. package/vendor/shared-buffer/src/lib.rs +58 -0
  58. package/vendor/shared-buffer/src/mmap.rs +250 -0
  59. package/vendor/shared-buffer/src/owned.rs +389 -0
  60. package/vendor/virtual-fs/Cargo.toml +181 -0
  61. package/vendor/virtual-fs/LICENSE +25 -0
  62. package/vendor/virtual-fs/src/arc_box_file.rs +142 -0
  63. package/vendor/virtual-fs/src/arc_file.rs +182 -0
  64. package/vendor/virtual-fs/src/arc_fs.rs +68 -0
  65. package/vendor/virtual-fs/src/buffer_file.rs +103 -0
  66. package/vendor/virtual-fs/src/builder.rs +232 -0
  67. package/vendor/virtual-fs/src/combine_file.rs +101 -0
  68. package/vendor/virtual-fs/src/cow_file.rs +345 -0
  69. package/vendor/virtual-fs/src/dual_write_file.rs +113 -0
  70. package/vendor/virtual-fs/src/empty_fs.rs +81 -0
  71. package/vendor/virtual-fs/src/filesystems.rs +108 -0
  72. package/vendor/virtual-fs/src/host_fs.rs +1390 -0
  73. package/vendor/virtual-fs/src/lib.rs +782 -0
  74. package/vendor/virtual-fs/src/limiter.rs +252 -0
  75. package/vendor/virtual-fs/src/mem_fs/file.rs +1799 -0
  76. package/vendor/virtual-fs/src/mem_fs/file_opener.rs +941 -0
  77. package/vendor/virtual-fs/src/mem_fs/filesystem.rs +2134 -0
  78. package/vendor/virtual-fs/src/mem_fs/mod.rs +245 -0
  79. package/vendor/virtual-fs/src/mem_fs/offloaded_file.rs +474 -0
  80. package/vendor/virtual-fs/src/mem_fs/stdio.rs +318 -0
  81. package/vendor/virtual-fs/src/mount_fs.rs +2225 -0
  82. package/vendor/virtual-fs/src/null_file.rs +87 -0
  83. package/vendor/virtual-fs/src/ops.rs +364 -0
  84. package/vendor/virtual-fs/src/overlay_fs.rs +2216 -0
  85. package/vendor/virtual-fs/src/passthru_fs.rs +119 -0
  86. package/vendor/virtual-fs/src/pipe.rs +603 -0
  87. package/vendor/virtual-fs/src/random_file.rs +88 -0
  88. package/vendor/virtual-fs/src/special_file.rs +108 -0
  89. package/vendor/virtual-fs/src/static_file.rs +133 -0
  90. package/vendor/virtual-fs/src/static_fs.rs +460 -0
  91. package/vendor/virtual-fs/src/tmp_fs.rs +95 -0
  92. package/vendor/virtual-fs/src/trace_fs.rs +258 -0
  93. package/vendor/virtual-fs/src/webc_volume_fs.rs +829 -0
  94. package/vendor/virtual-fs/src/zero_file.rs +90 -0
@@ -0,0 +1,167 @@
1
+ use crate::RunError;
2
+ use std::sync::{Arc, Mutex};
3
+ use virtual_fs::{FsError, limiter::FsMemoryLimiter};
4
+
5
+ #[derive(Debug)]
6
+ pub(crate) struct VfsQuota {
7
+ state: Mutex<VfsQuotaState>,
8
+ }
9
+
10
+ #[derive(Debug)]
11
+ struct VfsQuotaState {
12
+ bytes: usize,
13
+ entries: usize,
14
+ byte_ceiling: Option<usize>,
15
+ entry_ceiling: Option<usize>,
16
+ peak_bytes: usize,
17
+ peak_entries: usize,
18
+ exceeded: bool,
19
+ }
20
+
21
+ #[derive(Clone, Copy, Debug, PartialEq, Eq)]
22
+ pub(crate) struct VfsMetrics {
23
+ pub bytes: u64,
24
+ pub entries: u64,
25
+ }
26
+
27
+ impl VfsQuota {
28
+ pub fn new() -> Arc<Self> {
29
+ Arc::new(Self {
30
+ state: Mutex::new(VfsQuotaState {
31
+ bytes: 0,
32
+ entries: 0,
33
+ byte_ceiling: None,
34
+ entry_ceiling: None,
35
+ peak_bytes: 0,
36
+ peak_entries: 0,
37
+ exceeded: false,
38
+ }),
39
+ })
40
+ }
41
+
42
+ /// Seal the mounted filesystem as the baseline and allow only the supplied
43
+ /// additional live file data and inode counts during guest execution.
44
+ pub fn seal(&self, write_bytes: usize, entries: usize) -> Result<(), RunError> {
45
+ let mut state = self
46
+ .state
47
+ .lock()
48
+ .map_err(|error| RunError::Runtime(error.to_string()))?;
49
+ if state.byte_ceiling.is_some() || state.entry_ceiling.is_some() {
50
+ return Err(RunError::Runtime(
51
+ "runtime filesystem quota was sealed more than once".to_string(),
52
+ ));
53
+ }
54
+ state.byte_ceiling = Some(state.bytes.checked_add(write_bytes).ok_or_else(|| {
55
+ RunError::InvalidRequest("filesystem byte quota overflows host range".to_string())
56
+ })?);
57
+ state.entry_ceiling = Some(state.entries.checked_add(entries).ok_or_else(|| {
58
+ RunError::InvalidRequest("filesystem entry quota overflows host range".to_string())
59
+ })?);
60
+ state.peak_bytes = state.bytes;
61
+ state.peak_entries = state.entries;
62
+ Ok(())
63
+ }
64
+
65
+ pub fn exceeded(&self) -> bool {
66
+ self.state.lock().expect("VFS quota lock poisoned").exceeded
67
+ }
68
+
69
+ pub fn metrics(&self) -> VfsMetrics {
70
+ let state = self.state.lock().expect("VFS quota lock poisoned");
71
+ VfsMetrics {
72
+ bytes: state.peak_bytes as u64,
73
+ entries: state.peak_entries as u64,
74
+ }
75
+ }
76
+ }
77
+
78
+ impl FsMemoryLimiter for VfsQuota {
79
+ fn on_grow(&self, grown_bytes: usize) -> Result<(), FsError> {
80
+ let mut state = self.state.lock().map_err(|_| FsError::Lock)?;
81
+ let next = state
82
+ .bytes
83
+ .checked_add(grown_bytes)
84
+ .ok_or(FsError::WriteZero)?;
85
+ if state.byte_ceiling.is_some_and(|limit| next > limit) {
86
+ state.exceeded = true;
87
+ return Err(FsError::WriteZero);
88
+ }
89
+ state.bytes = next;
90
+ state.peak_bytes = state.peak_bytes.max(next);
91
+ Ok(())
92
+ }
93
+
94
+ fn on_shrink(&self, shrunk_bytes: usize) {
95
+ let mut state = self.state.lock().expect("VFS quota lock poisoned");
96
+ state.bytes = state
97
+ .bytes
98
+ .checked_sub(shrunk_bytes)
99
+ .expect("VFS byte accounting underflow");
100
+ }
101
+
102
+ fn on_create_entry(&self) -> Result<(), FsError> {
103
+ let mut state = self.state.lock().map_err(|_| FsError::Lock)?;
104
+ let next = state.entries.checked_add(1).ok_or(FsError::WriteZero)?;
105
+ if state.entry_ceiling.is_some_and(|limit| next > limit) {
106
+ state.exceeded = true;
107
+ return Err(FsError::WriteZero);
108
+ }
109
+ state.entries = next;
110
+ state.peak_entries = state.peak_entries.max(next);
111
+ Ok(())
112
+ }
113
+
114
+ fn on_remove_entry(&self) {
115
+ let mut state = self.state.lock().expect("VFS quota lock poisoned");
116
+ state.entries = state
117
+ .entries
118
+ .checked_sub(1)
119
+ .expect("VFS entry accounting underflow");
120
+ }
121
+ }
122
+
123
+ #[cfg(test)]
124
+ mod tests {
125
+ use super::*;
126
+
127
+ #[test]
128
+ fn seals_a_baseline_and_trips_sticky_limits() {
129
+ let quota = VfsQuota::new();
130
+ quota.on_grow(10).unwrap();
131
+ quota.on_create_entry().unwrap();
132
+ quota.seal(4, 1).unwrap();
133
+
134
+ quota.on_grow(4).unwrap();
135
+ quota.on_create_entry().unwrap();
136
+ assert_eq!(
137
+ quota.metrics(),
138
+ VfsMetrics {
139
+ bytes: 14,
140
+ entries: 2
141
+ }
142
+ );
143
+ assert_eq!(quota.on_grow(1), Err(FsError::WriteZero));
144
+ assert!(quota.exceeded());
145
+ }
146
+
147
+ #[test]
148
+ fn deletion_releases_live_storage_headroom() {
149
+ let quota = VfsQuota::new();
150
+ quota.on_grow(10).unwrap();
151
+ quota.on_create_entry().unwrap();
152
+ quota.seal(4, 1).unwrap();
153
+ quota.on_shrink(10);
154
+ quota.on_remove_entry();
155
+
156
+ quota.on_grow(14).unwrap();
157
+ quota.on_create_entry().unwrap();
158
+ assert!(!quota.exceeded());
159
+ assert_eq!(
160
+ quota.metrics(),
161
+ VfsMetrics {
162
+ bytes: 14,
163
+ entries: 1
164
+ }
165
+ );
166
+ }
167
+ }
@@ -0,0 +1,297 @@
1
+ use crate::filesystem::immutable_compiler_files;
2
+ use crate::{
3
+ CompilePipelineResponse, CompilerToolchain, GoCompilerSessionConfig, GoCompilerSessionRequest,
4
+ GoCompilerSessionResponse, GoCompilerSourceDelta, RunError, RunFailure,
5
+ };
6
+ use serde_bytes::ByteBuf;
7
+ use std::collections::BTreeMap;
8
+ use std::sync::{Arc, Mutex};
9
+ use virtual_fs::FileSystem;
10
+
11
+ #[derive(Debug, Default)]
12
+ struct SourceState {
13
+ generation: u32,
14
+ files: BTreeMap<String, ByteBuf>,
15
+ in_flight: bool,
16
+ }
17
+
18
+ /// A digest-bound, process-local Go compiler handle.
19
+ ///
20
+ /// The WebC package and Go standard library cross the JS/Wasm boundary once at
21
+ /// construction. Each later request carries only a monotonic `/work` source
22
+ /// delta and stage argv. The immutable filesystem is shared by copy-on-write
23
+ /// build overlays, while stores, instances, process state, outputs, and source
24
+ /// snapshots remain isolated per build.
25
+ pub struct GoCompilerSession {
26
+ digest: String,
27
+ toolchain: CompilerToolchain,
28
+ standard_library: Arc<dyn FileSystem + Send + Sync>,
29
+ source: Mutex<SourceState>,
30
+ }
31
+
32
+ impl GoCompilerSession {
33
+ pub fn new(config: GoCompilerSessionConfig) -> Result<Self, RunError> {
34
+ validate_digest(&config.digest)?;
35
+ if config.standard_library_files.is_empty() {
36
+ return Err(RunError::InvalidRequest(
37
+ "Go compiler session requires a non-empty standard library".to_string(),
38
+ ));
39
+ }
40
+ crate::run::validate_mounted_files(&config.standard_library_files, "Go standard library")?;
41
+ for path in config.standard_library_files.keys() {
42
+ validate_standard_library_path(path)?;
43
+ }
44
+ let standard_library = immutable_compiler_files(config.standard_library_files)?;
45
+ let toolchain = CompilerToolchain::new(config.toolchain)?;
46
+ Ok(Self {
47
+ digest: config.digest,
48
+ toolchain,
49
+ standard_library,
50
+ source: Mutex::new(SourceState::default()),
51
+ })
52
+ }
53
+
54
+ pub fn digest(&self) -> &str {
55
+ &self.digest
56
+ }
57
+
58
+ pub fn generation(&self) -> Result<u32, RunError> {
59
+ Ok(self
60
+ .source
61
+ .lock()
62
+ .map_err(|error| RunError::Runtime(error.to_string()))?
63
+ .generation)
64
+ }
65
+
66
+ pub async fn compile_pipeline_response(
67
+ &self,
68
+ request: GoCompilerSessionRequest,
69
+ ) -> Result<GoCompilerSessionResponse, RunError> {
70
+ if request.digest != self.digest {
71
+ return Err(RunError::InvalidRequest(
72
+ "Go compiler request digest does not match its hydrated session".to_string(),
73
+ ));
74
+ }
75
+ let generation = request.source_delta.generation;
76
+ let next_files = self.prepare_source_delta(request.source_delta)?;
77
+ let result = self
78
+ .toolchain
79
+ .compile_pipeline_with_base(
80
+ next_files.clone(),
81
+ request.stages,
82
+ self.standard_library.clone(),
83
+ )
84
+ .await;
85
+ let response = match result {
86
+ Ok(result) => CompilePipelineResponse {
87
+ ok: true,
88
+ result: Some(result),
89
+ error: None,
90
+ },
91
+ Err(error) => CompilePipelineResponse {
92
+ ok: false,
93
+ result: None,
94
+ error: Some(RunFailure {
95
+ code: error.code(),
96
+ message: error.to_string(),
97
+ }),
98
+ },
99
+ };
100
+ self.commit_source_delta(generation, next_files)?;
101
+ Ok(GoCompilerSessionResponse {
102
+ digest: self.digest.clone(),
103
+ generation,
104
+ response,
105
+ })
106
+ }
107
+
108
+ fn prepare_source_delta(
109
+ &self,
110
+ delta: GoCompilerSourceDelta,
111
+ ) -> Result<BTreeMap<String, ByteBuf>, RunError> {
112
+ let mut state = self
113
+ .source
114
+ .lock()
115
+ .map_err(|error| RunError::Runtime(error.to_string()))?;
116
+ if state.in_flight {
117
+ return Err(RunError::InvalidRequest(
118
+ "Go compiler session accepts one build at a time".to_string(),
119
+ ));
120
+ }
121
+ let next = apply_source_delta(&state, delta)?;
122
+ state.in_flight = true;
123
+ Ok(next)
124
+ }
125
+
126
+ fn commit_source_delta(
127
+ &self,
128
+ generation: u32,
129
+ files: BTreeMap<String, ByteBuf>,
130
+ ) -> Result<(), RunError> {
131
+ let mut state = self
132
+ .source
133
+ .lock()
134
+ .map_err(|error| RunError::Runtime(error.to_string()))?;
135
+ if !state.in_flight || generation != state.generation.saturating_add(1) {
136
+ return Err(RunError::Runtime(
137
+ "Go compiler session source state changed during a build".to_string(),
138
+ ));
139
+ }
140
+ state.generation = generation;
141
+ state.files = files;
142
+ state.in_flight = false;
143
+ Ok(())
144
+ }
145
+ }
146
+
147
+ fn apply_source_delta(
148
+ state: &SourceState,
149
+ delta: GoCompilerSourceDelta,
150
+ ) -> Result<BTreeMap<String, ByteBuf>, RunError> {
151
+ let expected = state.generation.checked_add(1).ok_or_else(|| {
152
+ RunError::InvalidRequest("Go compiler session generation is exhausted".to_string())
153
+ })?;
154
+ if delta.generation != expected {
155
+ return Err(RunError::InvalidRequest(format!(
156
+ "Go compiler source delta generation {} does not follow {}",
157
+ delta.generation, state.generation
158
+ )));
159
+ }
160
+ crate::run::validate_mounted_files(&delta.upsert_files, "Go source delta")?;
161
+ for path in delta.upsert_files.keys() {
162
+ validate_source_path(path)?;
163
+ }
164
+ let mut previous_remove = None;
165
+ for path in &delta.remove_paths {
166
+ validate_source_path(path)?;
167
+ if previous_remove.is_some_and(|previous: &String| previous >= path) {
168
+ return Err(RunError::InvalidRequest(
169
+ "Go compiler source removals must be strictly sorted and unique".to_string(),
170
+ ));
171
+ }
172
+ if delta.upsert_files.contains_key(path) {
173
+ return Err(RunError::InvalidRequest(format!(
174
+ "Go compiler source delta both removes and upserts '{path}'"
175
+ )));
176
+ }
177
+ previous_remove = Some(path);
178
+ }
179
+ let mut next = state.files.clone();
180
+ for path in delta.remove_paths {
181
+ next.remove(&path);
182
+ }
183
+ next.extend(delta.upsert_files);
184
+ crate::run::validate_mounted_files(&next, "Go source snapshot")?;
185
+ Ok(next)
186
+ }
187
+
188
+ fn validate_digest(digest: &str) -> Result<(), RunError> {
189
+ if digest.len() != 64
190
+ || !digest
191
+ .bytes()
192
+ .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
193
+ {
194
+ return Err(RunError::InvalidRequest(
195
+ "Go compiler session digest must be a lowercase SHA-256".to_string(),
196
+ ));
197
+ }
198
+ Ok(())
199
+ }
200
+
201
+ fn validate_standard_library_path(path: &str) -> Result<(), RunError> {
202
+ if !crate::filesystem::is_normalized_guest_path(path) || !path.starts_with("/go/") {
203
+ return Err(RunError::InvalidRequest(format!(
204
+ "Go standard-library file must be under /go: {path}"
205
+ )));
206
+ }
207
+ Ok(())
208
+ }
209
+
210
+ fn validate_source_path(path: &str) -> Result<(), RunError> {
211
+ if !crate::filesystem::is_normalized_guest_path(path) || !path.starts_with("/work/") {
212
+ return Err(RunError::InvalidRequest(format!(
213
+ "Go compiler mutable file must be under /work: {path}"
214
+ )));
215
+ }
216
+ Ok(())
217
+ }
218
+
219
+ #[cfg(test)]
220
+ mod tests {
221
+ use super::{
222
+ SourceState, apply_source_delta, validate_digest, validate_source_path,
223
+ validate_standard_library_path,
224
+ };
225
+ use crate::{GoCompilerSourceDelta, RunError};
226
+ use serde_bytes::ByteBuf;
227
+ use std::collections::BTreeMap;
228
+
229
+ #[test]
230
+ fn validates_digest_and_filesystem_boundaries() {
231
+ assert!(validate_digest(&"a".repeat(64)).is_ok());
232
+ assert!(validate_digest(&"A".repeat(64)).is_err());
233
+ assert!(validate_digest("short").is_err());
234
+ assert!(validate_standard_library_path("/go/pkg/fmt.a").is_ok());
235
+ assert!(validate_standard_library_path("/work/fmt.a").is_err());
236
+ assert!(validate_source_path("/work/main.go").is_ok());
237
+ assert!(validate_source_path("/go/pkg/fmt.a").is_err());
238
+ }
239
+
240
+ #[test]
241
+ fn source_state_starts_empty_and_generation_zero() {
242
+ let state = SourceState::default();
243
+ assert_eq!(state.generation, 0);
244
+ assert!(state.files.is_empty());
245
+ assert!(!state.in_flight);
246
+ }
247
+
248
+ #[test]
249
+ fn source_delta_applies_upserts_and_sorted_removals_monotonically() {
250
+ let state = SourceState {
251
+ generation: 1,
252
+ files: BTreeMap::from([
253
+ ("/work/main.go".to_string(), ByteBuf::from(vec![1])),
254
+ ("/work/old.go".to_string(), ByteBuf::from(vec![2])),
255
+ ]),
256
+ in_flight: false,
257
+ };
258
+ let delta = GoCompilerSourceDelta {
259
+ generation: 2,
260
+ upsert_files: BTreeMap::from([
261
+ ("/work/main.go".to_string(), ByteBuf::from(vec![3])),
262
+ ("/work/new.go".to_string(), ByteBuf::from(vec![4])),
263
+ ]),
264
+ remove_paths: vec!["/work/old.go".to_string()],
265
+ };
266
+ let next = apply_source_delta(&state, delta).unwrap();
267
+ assert_eq!(
268
+ next.keys().map(String::as_str).collect::<Vec<_>>(),
269
+ ["/work/main.go", "/work/new.go"]
270
+ );
271
+ assert_eq!(next["/work/main.go"].as_ref(), &[3]);
272
+ }
273
+
274
+ #[test]
275
+ fn source_delta_rejects_stale_generations_and_ambiguous_updates() {
276
+ let state = SourceState::default();
277
+ let stale = GoCompilerSourceDelta {
278
+ generation: 2,
279
+ upsert_files: BTreeMap::new(),
280
+ remove_paths: Vec::new(),
281
+ };
282
+ assert!(apply_source_delta(&state, stale).is_err());
283
+
284
+ let ambiguous = GoCompilerSourceDelta {
285
+ generation: 1,
286
+ upsert_files: BTreeMap::from([("/work/main.go".to_string(), ByteBuf::from(vec![1]))]),
287
+ remove_paths: vec!["/work/main.go".to_string()],
288
+ };
289
+ assert!(apply_source_delta(&state, ambiguous).is_err());
290
+ }
291
+
292
+ #[test]
293
+ fn invalid_request_error_code_remains_stable() {
294
+ let error = validate_source_path("../main.go").unwrap_err();
295
+ assert!(matches!(error, RunError::InvalidRequest(_)));
296
+ }
297
+ }