diff --git a/CHANGELOG.md b/CHANGELOG.md index e3c33eaba..4bc31dc38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,25 @@ All notable changes to UnoSim. Versions follow [Semantic Versioning](https://sem questions and dialogs fall back to the free Tutor without a Course Content session instead of failing with `INVALID_REQUEST` (Course Content SSOT §11, case G). +- **Sketches with `typedef` types, constant array sizes or default arguments + in function signatures run.** The generated function prototypes now sit + directly before the first function the sketch defines, without default + arguments, as in the Arduino IDE. Before, they stood at the top of the + file, so `Vektor add(Vektor a, Vektor b)`, `void f(double m[M][N])` and + `void f(byte precision = 2)` compiled with the Arduino compiler but failed + in the simulation. +- **An open `INPUT_PULLUP` pin reads `HIGH`.** The simulation now models the + internal pull-up resistor; before, such a pin read `LOW` as if a button + were pressed. + +### Documentation + +- The user guide lists the known differences between the simulation and a + real Arduino Uno (section "Grenzen der Simulation"): `int` is 32 bits wide + instead of 16, `long` and `double` are 64 bits, `Serial.print` of a float + without a precision prints `2.5` instead of `2.50`, `Serial.parseInt()` + returns at once instead of waiting for its timeout, and loop speed, memory + size and `random()` sequences differ. Point students to that section. ## v1.0.0 – 2026-10-09 diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md index 9f72fa970..48c69fd6c 100644 --- a/docs/USER_GUIDE.md +++ b/docs/USER_GUIDE.md @@ -108,6 +108,38 @@ Unter **File → Settings** findest du außerdem: - **Pin Monitor**: Pin-Übersicht über dem Board, - **Toast Duration**: Anzeigedauer von Hinweisen. +## Grenzen der Simulation + +UnoSim prüft jeden Sketch zuerst mit dem echten Arduino-Compiler für den Uno. +Was hier kompiliert, kompiliert also auch auf der Hardware. Ausgeführt wird der +Sketch danach aber nicht als Uno-Programm, sondern als Programm für den +UnoSim-Server. Dadurch weicht die Simulation in einigen Punkten vom echten +Arduino Uno ab: + +| Thema | Arduino Uno | UnoSim | +|---|---|---| +| Größe von `int` und `unsigned int` | 16 Bit: `int` reicht von −32768 bis 32767 | 32 Bit: `32767 + 1` ergibt 32768 statt −32768 | +| Größe von `long` | 32 Bit | 64 Bit | +| Größe von `double` | 32 Bit, gleich `float` | 64 Bit, rechnet genauer | +| `sizeof` | z. B. `sizeof(int)` = 2 | zeigt die Größen des Servers, z. B. 4 | +| `Serial.print` einer Kommazahl ohne Stellenangabe | `2.50` (immer 2 Nachkommastellen) | `2.5` | +| `Serial.parseInt()` ohne Eingabe | wartet 1 Sekunde, liefert dann 0 | liefert sofort 0 | +| Geschwindigkeit | 16 MHz | anders: Wie oft `loop()` pro Sekunde läuft, weicht ab. `millis()` und `delay()` stimmen. | +| Arbeitsspeicher | 2 KB | praktisch unbegrenzt: Ein großes Array kann hier laufen und auf dem Uno abstürzen | +| `random()` | feste Zahlenfolge des Uno | andere Zahlenfolge | + +Was das für deine Programme bedeutet: + +- Überläufe beobachtest du zuverlässig mit `byte` (0 bis 255); bei `int` + und `long` zeigt UnoSim die Grenzen des Uno nicht. +- Gib Kommazahlen immer mit Stellenzahl aus, z. B. `Serial.println(wert, 2)`; + dann ist die Ausgabe auf Uno und UnoSim gleich. +- Prüfe Programme, die von Wertebereichen, Speicher oder genauer Rechenzeit + abhängen, zusätzlich auf einem echten Board. + +Gesendeter Text im Serial Monitor endet immer mit dem Zeilenende (Code 10), +wie im Serial Monitor der Arduino IDE mit der Einstellung „New Line“. + ## Wenn etwas nicht klappt | Meldung oder Verhalten | Ursache und Lösung | diff --git a/server/mocks/arduino-mock/arduino-registry-logic.ts b/server/mocks/arduino-mock/arduino-registry-logic.ts index df6209bb1..821bfc144 100644 --- a/server/mocks/arduino-mock/arduino-registry-logic.ts +++ b/server/mocks/arduino-mock/arduino-registry-logic.ts @@ -65,11 +65,18 @@ void outputIORegistry() { // GPIO Functions with state tracking void pinMode(int pin, int mode) { if (pin >= 0 && pin < 20) { + bool enablesPullup = mode == INPUT_PULLUP && pinModes[pin] != INPUT_PULLUP; pinModes[pin] = mode; // Send pin state update via stderr (special protocol) { std::lock_guard lock(cerrMutex); std::cerr << "[[PIN_MODE:" << pin << ":" << mode << "]]" << std::endl; } - + + // The internal pull-up resistor makes an open input read HIGH until it is pulled LOW externally. + if (enablesPullup && pinValues[pin].exchange(HIGH, std::memory_order_seq_cst) != HIGH) { + std::lock_guard lock(cerrMutex); + std::cerr << "[[PIN_VALUE:" << pin << ":" << HIGH << "]]" << std::endl; + } + // Track in I/O Registry - add pinMode as an operation with mode info if (ioRegistry.find(pin) != ioRegistry.end()) { ioRegistry[pin].defined = true; diff --git a/server/services/sketch-file-builder.ts b/server/services/sketch-file-builder.ts index f90260a60..67cd96a7a 100644 --- a/server/services/sketch-file-builder.ts +++ b/server/services/sketch-file-builder.ts @@ -13,6 +13,7 @@ import { detectSketchEntrypoints } from "@shared/utils/sketch-validation"; import { isSafeHeaderName } from "@shared/input-limits"; import { resolvePathWithinRoot } from "../security/safe-paths"; import { normalizeSourcePath, resolveSourceProject } from "@shared/source-project"; +import { insertForwardDeclarations, planForwardDeclarations } from "./sketch-prototypes"; interface SketchBuildResult { sketchDir: string; @@ -21,18 +22,7 @@ interface SketchBuildResult { } export function extractForwardDeclarations(code: string): string { - const stripped = code.replaceAll(/\/\/[^\n]*/g, "").replaceAll(/\/\*[\s\S]*?\*\//g, ""); - const skipped = new Set(["if", "else", "while", "for", "do", "switch", "case", "return", "break", "continue", "goto", "class", "struct", "union", "enum", "namespace", "typedef", "setup", "loop", "main"]); - const definition = /^((?:\w[\w*&]*\s+)*\w[\w*&]*)\s+(\w+)\s*(\([^)]*\))\s*\{/gm; - const seen = new Set(); - const declarations: string[] = []; - for (const match of stripped.matchAll(definition)) { - const signature = `${match[2]}${match[3]}`; - if (skipped.has(match[2]) || seen.has(signature)) continue; - seen.add(signature); - declarations.push(`${match[1].trim()} ${match[2]}${match[3]};`); - } - return declarations.join("\n"); + return planForwardDeclarations(code).declarations; } export class SketchFileBuilder { @@ -76,7 +66,7 @@ export class SketchFileBuilder { } const footer = this.buildFooter(hasSetup, hasLoop); - const projectSource = entryFile + const project = entryFile ? resolveSourceProject({ entryFile, files: { @@ -86,16 +76,17 @@ export class SketchFileBuilder { return files; }, {}), }, - }).source - : code; + }) + : undefined; + const projectSource = project ? project.source : code; + // Removing the include keeps every line in place, so lineOrigins still match. const cleanedCode = projectSource.replaceAll(/#include\s*[<"]Arduino\.h[>"]/g, ""); + const isSketchLine = project + ? (lineIndex: number) => project.lineOrigins[lineIndex]?.file === project.entryFile + : undefined; + const userCode = insertForwardDeclarations(cleanedCode, isSketchLine); - const forwardDecls = this.extractForwardDeclarations(cleanedCode); - const forwardSection = forwardDecls - ? `// --- Forward declarations (auto-generated, mirrors Arduino IDE behaviour) ---\n${forwardDecls}\n\n` - : ""; - - const combined = `#include \n\n${forwardSection}// --- User code follows ---\n${cleanedCode}\n\n// --- Footer ---\n${footer}`; + const combined = `#include \n\n// --- User code follows ---\n${userCode}\n\n// --- Footer ---\n${footer}`; await writeFile(join(sketchDir, "Arduino.h"), ARDUINO_MOCK_CODE); await writeFile(sketchFile, combined); @@ -121,18 +112,6 @@ export class SketchFileBuilder { this.createdSketchDirs.delete(dir); } - /** - * Extracts forward declarations for all user-defined functions. - * - * The Arduino IDE automatically generates prototypes for every function in a - * sketch so that helper functions can be called before they are defined (just - * like in a real Arduino sketch). We replicate that behaviour here so that - * the local g++ compiler accepts the same code the Arduino IDE would accept. - */ - private extractForwardDeclarations(code: string): string { - return extractForwardDeclarations(code); - } - /** * Generates the main() wrapper based on presence of setup() and loop() */ diff --git a/server/services/sketch-prototypes.ts b/server/services/sketch-prototypes.ts new file mode 100644 index 000000000..bc044f833 --- /dev/null +++ b/server/services/sketch-prototypes.ts @@ -0,0 +1,146 @@ +/** + * Arduino-IDE-compatible function prototypes for the local g++ build. + * + * The Arduino IDE declares every sketch function before the first function + * definition, so a helper may be called before it is defined. The prototypes + * go there, not at the top of the file, because their signatures may use + * types and constants that the sketch defines first (a `typedef`, or `M` in + * `double mat[M][N]`). Default arguments are left out: C++ allows them only + * once, and the definition already carries them. + */ + +import { maskCommentsPreservingLines } from "@shared/source-project"; + +const FUNCTION_DEFINITION = /^((?:\w[\w*&]*\s+)*\w[\w*&]*)\s+(\w+)\s*(\([^)]*\))\s*\{/gm; +const NOT_A_FUNCTION = new Set(["if", "else", "while", "for", "do", "switch", "case", "return", "break", "continue", "goto", "class", "struct", "union", "enum", "namespace", "typedef"]); +const ENTRYPOINTS = new Set(["setup", "loop", "main"]); +const OPENING_BRACKETS = "([{"; +const CLOSING_BRACKETS = ")]}"; + +export interface ForwardDeclarationPlan { + /** Newline-separated prototypes, empty when the sketch needs none. */ + declarations: string; + /** Offset in the code where the prototypes belong; -1 when there is no function definition. */ + insertAt: number; +} + +interface ParameterScan { + parts: string[]; + current: string; + depth: number; + quote: string | null; + escaped: boolean; +} + +function scanQuoted(scan: ParameterScan, character: string): void { + scan.current += character; + if (scan.escaped) scan.escaped = false; + else if (character === "\\") scan.escaped = true; + else if (character === scan.quote) scan.quote = null; +} + +function scanParameterCharacter(scan: ParameterScan, character: string): void { + if (scan.quote) { + scanQuoted(scan, character); + return; + } + if (character === "," && scan.depth === 0) { + scan.parts.push(scan.current); + scan.current = ""; + return; + } + if (character === '"' || character === "'") scan.quote = character; + else if (OPENING_BRACKETS.includes(character)) scan.depth++; + else if (CLOSING_BRACKETS.includes(character)) scan.depth--; + scan.current += character; +} + +/** Index of the `=` that starts a default argument, ignoring `=` inside quotes or brackets. */ +function defaultArgumentStart(parameter: string): number { + let depth = 0; + let quote: string | null = null; + for (let index = 0; index < parameter.length; index++) { + const character = parameter[index]; + if (quote) { + if (character === "\\") index++; + else if (character === quote) quote = null; + } else if (character === '"' || character === "'") { + quote = character; + } else if (OPENING_BRACKETS.includes(character)) { + depth++; + } else if (CLOSING_BRACKETS.includes(character)) { + depth--; + } else if (character === "=" && depth === 0) { + return index; + } + } + return -1; +} + +/** Removes default arguments from a parenthesized parameter list such as `(int a, byte b = 2)`. */ +export function stripDefaultArguments(parameterList: string): string { + const scan: ParameterScan = { parts: [], current: "", depth: 0, quote: null, escaped: false }; + for (const character of parameterList.slice(1, -1)) scanParameterCharacter(scan, character); + scan.parts.push(scan.current); + if (!scan.parts.some((part) => defaultArgumentStart(part) >= 0)) return parameterList; + const parameters = scan.parts.map((part) => { + const start = defaultArgumentStart(part); + return (start >= 0 ? part.slice(0, start) : part).trim(); + }); + return `(${parameters.join(", ")})`; +} + +function lineIndexAt(code: string, offset: number): number { + let line = 0; + for (let index = code.indexOf("\n"); index >= 0 && index < offset; index = code.indexOf("\n", index + 1)) line++; + return line; +} + +/** + * Plans the prototypes for every function defined in `code`. + * + * `isSketchLine` tells which lines belong to the sketch itself rather than to + * an inlined header; the prototypes go before the first function the sketch + * defines, like in the Arduino IDE. Without a sketch function they go before + * the first function at all. + */ +export function planForwardDeclarations( + code: string, + isSketchLine: (lineIndex: number) => boolean = () => true, +): ForwardDeclarationPlan { + const masked = maskCommentsPreservingLines(code); + const seen = new Set(); + const declarations: string[] = []; + let firstDefinition = -1; + let firstSketchDefinition = -1; + + for (const match of masked.matchAll(FUNCTION_DEFINITION)) { + const name = match[2]; + if (NOT_A_FUNCTION.has(name)) continue; + const offset = match.index ?? 0; + if (firstDefinition < 0) firstDefinition = offset; + if (firstSketchDefinition < 0 && isSketchLine(lineIndexAt(masked, offset))) firstSketchDefinition = offset; + if (ENTRYPOINTS.has(name)) continue; + const parameters = stripDefaultArguments(match[3]); + const signature = `${name}${parameters}`; + if (seen.has(signature)) continue; + seen.add(signature); + declarations.push(`${match[1].trim()} ${signature};`); + } + + return { + declarations: declarations.join("\n"), + insertAt: firstSketchDefinition >= 0 ? firstSketchDefinition : firstDefinition, + }; +} + +/** Inserts the planned prototypes into `code`; returns `code` unchanged when none are needed. */ +export function insertForwardDeclarations( + code: string, + isSketchLine?: (lineIndex: number) => boolean, +): string { + const plan = planForwardDeclarations(code, isSketchLine); + if (!plan.declarations || plan.insertAt < 0) return code; + const section = `// --- Forward declarations (auto-generated, mirrors Arduino IDE behaviour) ---\n${plan.declarations}\n\n`; + return `${code.slice(0, plan.insertAt)}${section}${code.slice(plan.insertAt)}`; +} diff --git a/shared/source-project.ts b/shared/source-project.ts index bd6eba28c..b66522a3b 100644 --- a/shared/source-project.ts +++ b/shared/source-project.ts @@ -143,7 +143,8 @@ function advanceStringMask(state: StringMaskState, current: string): StringMaskS return state; } -function maskCommentsPreservingLines(source: string): string { +/** Replaces comment characters with spaces, keeping every offset and line break of `source`. */ +export function maskCommentsPreservingLines(source: string): string { const result: string[] = []; let inBlockComment = false; let stringState: StringMaskState = { quote: null, escaped: false }; diff --git a/tests/integration/sketch-runtime-fidelity.test.ts b/tests/integration/sketch-runtime-fidelity.test.ts new file mode 100644 index 000000000..a25336a61 --- /dev/null +++ b/tests/integration/sketch-runtime-fidelity.test.ts @@ -0,0 +1,113 @@ +import { execFile, spawn } from "node:child_process"; +import { mkdtemp, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { promisify } from "node:util"; +import { afterAll, beforeAll, describe, expect, it } from "vitest"; +import { SketchFileBuilder } from "../../server/services/sketch-file-builder"; + +const execFileAsync = promisify(execFile); +const RUN_TIMEOUT_MS = 10_000; + +function decodeSerial(stderr: string): string { + return [...stderr.matchAll(/\[\[SERIAL_EVENT:\d+:([A-Za-z0-9+/=]*)\]\]/g)] + .map((match) => Buffer.from(match[1], "base64").toString("utf8")) + .join(""); +} + +/** Builds a sketch like the local runner, runs it, and returns its serial output once it prints DONE. */ +async function runUntilDone( + root: string, + id: string, + code: string, + headers: Array<{ name: string; content: string }> = [], +): Promise { + const builder = new SketchFileBuilder(root); + const { sketchDir, sketchFile, exeFile } = await builder.build(code, id, headers, headers.length > 0 ? "sketch.ino" : undefined); + await execFileAsync("g++", ["-I", sketchDir, sketchFile, "-o", exeFile, "-pthread"]); + + return new Promise((resolve, reject) => { + const child = spawn(exeFile, [], { stdio: ["pipe", "ignore", "pipe"] }); + let stderr = ""; + const timer = setTimeout(() => { + child.kill("SIGKILL"); + reject(new Error(`Sketch ${id} did not print DONE; serial output: ${decodeSerial(stderr)}`)); + }, RUN_TIMEOUT_MS); + child.stderr.on("data", (chunk: Buffer) => { + stderr += chunk.toString("utf8"); + const serial = decodeSerial(stderr); + if (serial.includes("DONE")) { + clearTimeout(timer); + child.kill("SIGKILL"); + resolve(serial); + } + }); + }); +} + +describe("sketch runtime fidelity", () => { + let root: string; + + beforeAll(async () => { + root = await mkdtemp(join(tmpdir(), "unosim-runtime-fidelity-")); + }); + + afterAll(async () => { + await rm(root, { recursive: true, force: true }); + }); + + it("reads an open INPUT_PULLUP pin as HIGH", async () => { + const serial = await runUntilDone(root, "pullup", ` +void setup() { + Serial.begin(115200); + pinMode(2, INPUT_PULLUP); + Serial.print("PIN2="); + Serial.println(digitalRead(2)); + Serial.println("DONE"); +} +void loop() {} +`); + expect(serial).toContain("PIN2=1"); + }, 30_000); + + it("runs a sketch whose helper signatures use a typedef", async () => { + const serial = await runUntilDone(root, "typedef", ` +typedef struct vektor { float x; float y; } Vektor; +void setup() { + Serial.begin(115200); + Vektor a = {1, 2}; + Serial.println(addieren(a, a).y, 1); + Serial.println("DONE"); +} +void loop() {} +Vektor addieren(Vektor v, Vektor s) { v.x += s.x; v.y += s.y; return v; } +`); + expect(serial).toContain("4.0"); + }, 30_000); + + it("runs a sketch whose header uses constant array sizes and default arguments", async () => { + const header = ` +#pragma once +const int M = 2; +void printFirst(const double mat[M][M], const char *name = "M", byte precision = 2) { + Serial.print(name); + Serial.println(mat[0][0], precision); +} +`; + const serial = await runUntilDone(root, "matrix", ` +#include "mat.h" +double A[M][M] = {{1.5, 0}, {0, 1}}; +void setup() { + Serial.begin(115200); + scale(A, 2); + printFirst(A); + printFirst(A, "A", 1); + Serial.println("DONE"); +} +void loop() {} +void scale(double mat[M][M], double factor) { mat[0][0] *= factor; } +`, [{ name: "mat.h", content: header }]); + expect(serial).toContain("M3.00"); + expect(serial).toContain("A3.0"); + }, 30_000); +}); diff --git a/tests/server/services/sketch-file-builder.test.ts b/tests/server/services/sketch-file-builder.test.ts index bc6b22a0c..e2f1bba0a 100644 --- a/tests/server/services/sketch-file-builder.test.ts +++ b/tests/server/services/sketch-file-builder.test.ts @@ -95,11 +95,10 @@ void loop() {} void helper() {} `.trim(); const content = await buildSketch(code); - // Forward decl must appear BEFORE the user-code section + // Like the Arduino IDE: directly before the first function definition const fwdIdx = content.indexOf("void helper();"); - const userIdx = content.indexOf("// --- User code follows ---"); - expect(fwdIdx).toBeGreaterThan(-1); - expect(fwdIdx).toBeLessThan(userIdx); + expect(fwdIdx).toBeGreaterThan(content.indexOf("// --- User code follows ---")); + expect(fwdIdx).toBeLessThan(content.indexOf("void setup() {")); }); it("does not add forward declarations for setup() and loop()", async () => { @@ -136,11 +135,57 @@ void loop() {} int add(int left, int right) { return left + right; } `.trim(); const content = await buildSketch(code); - const declaration = "int add(int left, int right);"; - const declarationIndex = content.indexOf(declaration); - const userCodeIndex = content.indexOf("// --- User code follows ---"); + const declarationIndex = content.indexOf("int add(int left, int right);"); expect(declarationIndex).toBeGreaterThan(-1); - expect(declarationIndex).toBeLessThan(userCodeIndex); + expect(declarationIndex).toBeLessThan(content.indexOf("void setup() {")); + }); + + it("places prototypes after a typedef that their signatures use", async () => { + const code = ` +typedef struct vektor { float x; float y; } Vektor; +void setup() { Vektor v = addieren(v, v); } +void loop() {} +Vektor addieren(Vektor v, Vektor s) { return v; } +`.trim(); + const content = await buildSketch(code); + const declarationIndex = content.indexOf("Vektor addieren(Vektor v, Vektor s);"); + expect(declarationIndex).toBeGreaterThan(content.indexOf("} Vektor;")); + expect(declarationIndex).toBeLessThan(content.indexOf("void setup() {")); + }); + + it("places prototypes after constants used as array sizes", async () => { + const code = ` +const int M = 3; +void setup() {} +void loop() {} +void clear(double mat[M][M]) {} +`.trim(); + const content = await buildSketch(code); + const declarationIndex = content.indexOf("void clear(double mat[M][M]);"); + expect(declarationIndex).toBeGreaterThan(content.indexOf("const int M = 3;")); + }); + + it("leaves default arguments out of prototypes", async () => { + const code = ` +void setup() { show(); } +void loop() {} +void show(const char *name = "M", byte precision = 2) {} +`.trim(); + const content = await buildSketch(code); + expect(content).toContain("void show(const char *name, byte precision);"); + expect(content).toContain('void show(const char *name = "M", byte precision = 2) {}'); + }); + + it("places prototypes before the first sketch function, after an included header", async () => { + const content = await buildProjectSketch( + '#include "mat.h"\nvoid setup() { scale(2); }\nvoid loop() {}\nvoid scale(double factor) {}', + "main.ino", + [{ name: "mat.h", content: "const int M = 3;\nvoid show(int precision = 2) {}" }], + ); + const forwardIndex = content.indexOf("// --- Forward declarations"); + expect(forwardIndex).toBeGreaterThan(content.indexOf("void show(int precision = 2) {}")); + expect(forwardIndex).toBeLessThan(content.indexOf("void setup() {")); + expect(content).toContain("void scale(double factor);"); }); it("does not duplicate declarations for overloaded or repeated function names", async () => { diff --git a/tests/server/services/sketch-prototypes.test.ts b/tests/server/services/sketch-prototypes.test.ts new file mode 100644 index 000000000..e7eedbbb0 --- /dev/null +++ b/tests/server/services/sketch-prototypes.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, it } from "vitest"; +import { + insertForwardDeclarations, + planForwardDeclarations, + stripDefaultArguments, +} from "../../../server/services/sketch-prototypes"; + +describe("sketch prototypes", () => { + it("keeps a parameter list without default arguments unchanged", () => { + expect(stripDefaultArguments("(int left,int right)")).toBe("(int left,int right)"); + }); + + it("removes default arguments, including strings that contain commas or equals signs", () => { + expect(stripDefaultArguments('(const char *sep = ", =", byte n = 2, int last)')).toBe( + "(const char *sep, byte n, int last)", + ); + }); + + it("ignores functions that only appear in comments", () => { + const plan = planForwardDeclarations("/* int fake() { } */\nvoid setup() {}\nint real() { return 1; }"); + expect(plan.declarations).toBe("int real();"); + }); + + it("plans the insertion before the first function the sketch itself defines", () => { + const code = "void header() {}\nconst int M = 2;\nvoid setup() {}\nvoid helper(int a[M]) {}"; + const plan = planForwardDeclarations(code, (lineIndex) => lineIndex >= 1); + expect(plan.insertAt).toBe(code.indexOf("void setup()")); + expect(plan.declarations).toBe("void header();\nvoid helper(int a[M]);"); + }); + + it("returns code without function definitions unchanged", () => { + expect(insertForwardDeclarations("const int M = 2;\n")).toBe("const int M = 2;\n"); + }); +}); diff --git a/vitest.config.ts b/vitest.config.ts index 96f044189..54e56b281 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -112,6 +112,7 @@ export default defineConfig({ "tests/integration/serial-flow.test.ts", "tests/integration/compiler-canaries.test.ts", "tests/integration/compile-include-boundary.test.ts", + "tests/integration/sketch-runtime-fidelity.test.ts", "tests/integration/worker-pool.*.test.ts", "tests/integration/concurrent-50-clients.test.ts", "tests/server/services/scalability-stress.test.ts", @@ -153,6 +154,7 @@ export default defineConfig({ include: [ "tests/integration/compiler-canaries.test.ts", "tests/integration/compile-include-boundary.test.ts", + "tests/integration/sketch-runtime-fidelity.test.ts", "tests/server/telemetry-heartbeat-integration.test.ts", "tests/core/sandbox-stress.test.ts", ],