move TTC capture to unit test (#7268)
* move TTC capture to unit test * progress with artifact * fmt * abstract cases * add another case * add another test * update snapshots with proper file names * force to JSON * fmt * make jest happy * add another example and other tweaks * fix * tweak * add logs * more logs * strip out kcl version * remove logs * add comment explainer * more comments * more comment * remove package-lock line
This commit is contained in:
@ -11,6 +11,12 @@ const jestConfig: Config = {
|
|||||||
"^.+\.tsx?$": ["ts-jest",{ babelConfig: true }],
|
"^.+\.tsx?$": ["ts-jest",{ babelConfig: true }],
|
||||||
},
|
},
|
||||||
testEnvironment: "jest-fixed-jsdom",
|
testEnvironment: "jest-fixed-jsdom",
|
||||||
|
// Include both standard test patterns and our custom .jesttest. pattern
|
||||||
|
testMatch: [
|
||||||
|
"**/__tests__/**/*.[jt]s?(x)",
|
||||||
|
"**/?(*.)+(spec|test).[tj]s?(x)",
|
||||||
|
"**/?(*.)+(jesttest).[tj]s?(x)"
|
||||||
|
],
|
||||||
// TAG: paths, path, baseUrl, alias
|
// TAG: paths, path, baseUrl, alias
|
||||||
// This is necessary to use tsconfig path aliases.
|
// This is necessary to use tsconfig path aliases.
|
||||||
moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, { prefix: '<rootDir>/../' }),
|
moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, { prefix: '<rootDir>/../' }),
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import type { Binary as BSONBinary } from 'bson'
|
import type { Binary as BSONBinary } from 'bson'
|
||||||
import { v4 } from 'uuid'
|
import { v4 } from 'uuid'
|
||||||
import type { AnyMachineSnapshot } from 'xstate'
|
import type { AnyMachineSnapshot } from 'xstate'
|
||||||
import type { CallExpressionKw, SourceRange } from '@src/lang/wasm'
|
import type { CallExpressionKw, ExecState, SourceRange } from '@src/lang/wasm'
|
||||||
import { isDesktop } from '@src/lib/isDesktop'
|
import { isDesktop } from '@src/lib/isDesktop'
|
||||||
import type { AsyncFn } from '@src/lib/types'
|
import type { AsyncFn } from '@src/lib/types'
|
||||||
|
|
||||||
@ -522,6 +522,20 @@ export function getModuleId(sourceRange: SourceRange) {
|
|||||||
return sourceRange[2]
|
return sourceRange[2]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getModuleIdByFileName(
|
||||||
|
fileName: string,
|
||||||
|
fileNames: ExecState['filenames']
|
||||||
|
) {
|
||||||
|
const module = Object.entries(fileNames).find(
|
||||||
|
([, moduleInfo]) =>
|
||||||
|
moduleInfo?.type === 'Local' && moduleInfo.value === fileName
|
||||||
|
)
|
||||||
|
if (module) {
|
||||||
|
return Number(module[0]) // Return the module ID
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
export function getInVariableCase(name: string, prefixIfDigit = 'm') {
|
export function getInVariableCase(name: string, prefixIfDigit = 'm') {
|
||||||
// As of 2025-04-08, standard case for KCL variables is camelCase
|
// As of 2025-04-08, standard case for KCL variables is camelCase
|
||||||
const startsWithANumber = !Number.isNaN(Number(name.charAt(0)))
|
const startsWithANumber = !Number.isNaN(Number(name.charAt(0)))
|
||||||
|
198
src/machines/__snapshots__/modifyWithTTC.test.json
Normal file
198
src/machines/__snapshots__/modifyWithTTC.test.json
Normal file
@ -0,0 +1,198 @@
|
|||||||
|
{
|
||||||
|
"make this neon green please, use #39FF14": {
|
||||||
|
"prompt": "make this neon green please, use #39FF14",
|
||||||
|
"source_ranges": [
|
||||||
|
{
|
||||||
|
"prompt": "The users main selection is the wall of a general-sweep (that is an extrusion, revolve, sweep or loft).\nThe source range though is for the original segment before it was extruded, you can add a tag to that segment in order to refer to this wall, for example \"startSketchOn(someSweepVariable, face = segmentTag)\"\nBut it's also worth bearing in mind that the user may have intended to select the sweep itself, not this individual wall, see later source ranges for more context. about the sweep",
|
||||||
|
"range": {
|
||||||
|
"start": {
|
||||||
|
"line": 6,
|
||||||
|
"column": 5
|
||||||
|
},
|
||||||
|
"end": {
|
||||||
|
"line": 6,
|
||||||
|
"column": 32
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"file": "main.kcl"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt": "This is the sweep's source range from the user's main selection of the end cap.",
|
||||||
|
"range": {
|
||||||
|
"start": {
|
||||||
|
"line": 10,
|
||||||
|
"column": 13
|
||||||
|
},
|
||||||
|
"end": {
|
||||||
|
"line": 10,
|
||||||
|
"column": 46
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"file": "main.kcl"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"project_name": "test-project",
|
||||||
|
"files": {
|
||||||
|
"main.kcl": "import \"b.kcl\" as b\nsketch001 = startSketchOn(XZ)\nprofile001 = startProfile(sketch001, at = [57.81, 250.51])\n |> line(end = [121.13, 56.63], tag = $seg02)\n |> line(end = [83.37, -34.61], tag = $seg01)\n |> line(end = [19.66, -116.4])\n |> line(end = [-221.8, -41.69])\n |> line(endAbsolute = [profileStartX(%), profileStartY(%)])\n |> close()\nextrude001 = extrude(profile001, length = 200)\nsketch002 = startSketchOn(XZ)\n |> startProfile(at = [-73.64, -42.89])\n |> xLine(length = 173.71)\n |> line(end = [-22.12, -94.4])\n |> line(end = [-22.12, -50.4])\n |> line(end = [-22.12, -94.4])\n |> line(end = [-22.12, -50.4])\n |> xLine(length = -156.98)\n |> line(endAbsolute = [profileStartX(%), profileStartY(%)])\n |> close()\nextrude002 = extrude(sketch002, length = 50)\nb\n",
|
||||||
|
"b.kcl": "sketch003 = startSketchOn(XY)\n |> startProfile(at = [52.92, 157.81])\n |> angledLine(angle = 0, length = 176.4, tag = $rectangleSegmentA001)\n |> angledLine(\n angle = segAng(rectangleSegmentA001) - 90,\n length = 53.4,\n tag = $rectangleSegmentB001,\n )\n |> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001), tag = $rectangleSegmentC001)\n |> line(end = [-22.12, -50.4])\n |> line(end = [-22.12, -94.4])\n |> line(endAbsolute = [profileStartX(%), profileStartY(%)])\n |> close()\nextrude(sketch003, length = 20)\n"
|
||||||
|
},
|
||||||
|
"expectedFiles": {
|
||||||
|
"main.kcl": "import \"b.kcl\" as b\nsketch001 = startSketchOn(XZ)\nprofile001 = startProfile(sketch001, at = [57.81, 250.51])\n |> line(end = [121.13, 56.63], tag = $seg02)\n |> line(end = [83.37, -34.61], tag = $seg01)\n |> line(end = [19.66, -116.4])\n |> line(end = [-221.8, -41.69])\n |> line(endAbsolute = [profileStartX(%), profileStartY(%)])\n |> close()\nextrude001 = extrude(profile001, length = 200)\n |> appearance(color = \"#39FF14\")\nsketch002 = startSketchOn(XZ)\n |> startProfile(at = [-73.64, -42.89])\n |> xLine(length = 173.71)\n |> line(end = [-22.12, -94.4])\n |> line(end = [-22.12, -50.4])\n |> line(end = [-22.12, -94.4])\n |> line(end = [-22.12, -50.4])\n |> xLine(length = -156.98)\n |> line(endAbsolute = [profileStartX(%), profileStartY(%)])\n |> close()\nextrude002 = extrude(sketch002, length = 50)\nb\n",
|
||||||
|
"b.kcl": "sketch003 = startSketchOn(XY)\n |> startProfile(at = [52.92, 157.81])\n |> angledLine(angle = 0, length = 176.4, tag = $rectangleSegmentA001)\n |> angledLine(\n angle = segAng(rectangleSegmentA001) - 90,\n length = 53.4,\n tag = $rectangleSegmentB001,\n )\n |> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001), tag = $rectangleSegmentC001)\n |> line(end = [-22.12, -50.4])\n |> line(end = [-22.12, -94.4])\n |> line(endAbsolute = [profileStartX(%), profileStartY(%)])\n |> close()\nextrude(sketch003, length = 20)\n"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Change this to red please, #ff0000": {
|
||||||
|
"prompt": "Change this to red please, #ff0000",
|
||||||
|
"source_ranges": [
|
||||||
|
{
|
||||||
|
"prompt": "The users main selection is the wall of a general-sweep (that is an extrusion, revolve, sweep or loft).\nThe source range though is for the original segment before it was extruded, you can add a tag to that segment in order to refer to this wall, for example \"startSketchOn(someSweepVariable, face = segmentTag)\"\nBut it's also worth bearing in mind that the user may have intended to select the sweep itself, not this individual wall, see later source ranges for more context. about the sweep",
|
||||||
|
"range": {
|
||||||
|
"start": {
|
||||||
|
"line": 82,
|
||||||
|
"column": 5
|
||||||
|
},
|
||||||
|
"end": {
|
||||||
|
"line": 82,
|
||||||
|
"column": 35
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"file": "ball-bearing.kcl"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt": "This is the sweep's source range from the user's main selection of the end cap.",
|
||||||
|
"range": {
|
||||||
|
"start": {
|
||||||
|
"line": 90,
|
||||||
|
"column": 5
|
||||||
|
},
|
||||||
|
"end": {
|
||||||
|
"line": 90,
|
||||||
|
"column": 35
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"file": "ball-bearing.kcl"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"project_name": "test-project",
|
||||||
|
"files": {
|
||||||
|
"ball-bearing.kcl": "// Pillow Block Bearing\n// The ball bearing for the pillow block bearing assembly\n\n// Set units\n@settings(defaultLengthUnit = in)\n\n// Import Parameters\nimport * from \"parameters.kcl\"\n\n// Create the sketch of one of the balls. The ball diameter is sized as a fraction of the difference between inner and outer radius of the bearing\nballsSketch = startSketchOn(offsetPlane(XY, offset = stockThickness / 2))\n |> startProfile(at = [bearingBoreDiameter / 2 + 0.1, 0.001])\n |> arc(angleEnd = 0, angleStart = 180, radius = sphereDia / 2)\n |> close()\n\n// Revolve the ball to make a sphere and pattern around the inside wall\nballs = revolve(ballsSketch, axis = X)\n |> patternCircular3d(\n arcDegrees = 360,\n axis = [0, 0, 1],\n center = [0, 0, 0],\n instances = 16,\n rotateDuplicates = true,\n )\n\n// Create the sketch for the chain around the balls\nchainSketch = startSketchOn(offsetPlane(XY, offset = stockThickness / 2))\n |> startProfile(at = [\n bearingBoreDiameter / 2 + 0.1 + sphereDia / 2 - (chainWidth / 2),\n 0.125 * sin(60deg)\n ])\n |> arc(angleEnd = 60, angleStart = 120, radius = sphereDia / 2)\n |> line(end = [0, chainThickness])\n |> line(end = [-chainWidth, 0])\n |> close()\n\n// Revolve the chain sketch\nchainHead = revolve(chainSketch, axis = X)\n |> patternCircular3d(\n arcDegrees = 360,\n axis = [0, 0, 1],\n center = [0, 0, 0],\n instances = 16,\n rotateDuplicates = true,\n )\n\n// Create the sketch for the links in between the chains\nlinkSketch = startSketchOn(XZ)\n |> circle(\n center = [\n bearingBoreDiameter / 2 + 0.1 + sphereDia / 2,\n stockThickness / 2\n ],\n radius = linkDiameter / 2,\n )\n\n// Create the walls of the bearing\nbearingBody = startSketchOn(XZ)\nbearingUpper = startProfile(\n bearingBody,\n at = [\n bearingOuterDiameter / 2 - .07,\n stockThickness\n ],\n )\n |> angledLine(angle = -91, length = 0.05)\n |> xLine(length = -(bearingOuterDiameter / 2 - (bearingBoreDiameter / 2)) + .145)\n |> yLine(endAbsolute = 0.105)\n |> xLine(length = -0.025)\n |> angledLine(angle = 91, endAbsoluteY = profileStartY(%))\n |> line(endAbsolute = [profileStartX(%), profileStartY(%)])\n |> close()\n |> revolve(angle = 360, axis = Y)\n |> appearance(%, color = \"#121212\")\n\nbearingLower = startProfile(bearingBody, at = [bearingBoreDiameter / 2, 0.025])\n |> xLine(length = 0.05)\n |> angledLine(angle = 75, length = 0.04, tag = $seg01)\n |> xLine(length = 0.05)\n |> angledLine(angle = -75, length = segLen(seg01))\n |> xLine(endAbsolute = bearingOuterDiameter / 2)\n |> yLine(length = stockThickness)\n |> xLine(length = -0.07)\n |> angledLine(angle = -91, endAbsoluteY = profileStartY(%) + .075)\n |> xLine(endAbsolute = profileStartX(%) + .05)\n |> angledLine(angle = 91, endAbsoluteY = stockThickness * 1.25)\n |> xLine(endAbsolute = profileStartX(%))\n |> line(endAbsolute = [profileStartX(%), profileStartY(%)])\n |> close()\n |> revolve(angle = 360, axis = Y)\n |> appearance(%, color = \"#f0f0f0\")\n\n// Revolve the link sketch\nrevolve(linkSketch, axis = Y, angle = 360 / 16)\n |> patternCircular3d(\n arcDegrees = 360,\n axis = [0, 0, 1],\n center = [0, 0, 0],\n instances = 16,\n rotateDuplicates = true,\n )\n",
|
||||||
|
"block.kcl": "// Pillow Block Bearing\n// The machined block for the pillow block bearing assembly. The block is dimensioned using the bolt pattern spacing, and each bolt hole includes a counterbore\n\n// Set units\n@settings(defaultLengthUnit = in)\n\n// Import Parameters\nimport * from \"parameters.kcl\"\n\n// Calculate the dimensions of the block using the specified bolt spacing. The size of the block can be defined by adding a multiple of the counterbore diameter to the bolt spacing\nblockLength = boltSpacingX + counterboreDiameter + boltDiameter\nblockWidth = boltSpacingY + counterboreDiameter + boltDiameter\n\n// Draw the base plate\nplateSketch = startSketchOn(XY)\n |> startProfile(at = [-blockLength / 2, -blockWidth / 2])\n |> angledLine(angle = 0, length = blockLength, tag = $rectangleSegmentA001)\n |> angledLine(angle = segAng(rectangleSegmentA001) + 90, length = blockWidth, tag = $rectangleSegmentB001)\n |> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001), tag = $rectangleSegmentC001)\n |> line(endAbsolute = [profileStartX(%), profileStartY(%)], tag = $rectangleSegmentD001)\n |> close()\n |> subtract2d(tool = circle(center = [0, 0], radius = bearingOuterDiameter / 2))\nplateBody = extrude(plateSketch, length = stockThickness)\n |> appearance(%, color = \"#1e62eb\")\n |> fillet(\n radius = boltDiameter * 1 / 3,\n tags = [\n getNextAdjacentEdge(rectangleSegmentB001),\n getNextAdjacentEdge(rectangleSegmentA001),\n getNextAdjacentEdge(rectangleSegmentC001),\n getNextAdjacentEdge(rectangleSegmentD001)\n ],\n )\n\n// Define hole positions\nholePositions = [\n [-boltSpacingX / 2, -boltSpacingY / 2],\n [-boltSpacingX / 2, boltSpacingY / 2],\n [boltSpacingX / 2, -boltSpacingY / 2],\n [boltSpacingX / 2, boltSpacingY / 2]\n]\n\n// Function to create a counterbored hole\nfn counterbore(@holePosition) {\n cbBore = startSketchOn(plateBody, face = END)\n |> circle(center = holePosition, radius = counterboreDiameter / 2)\n |> extrude(length = -counterboreDepth)\n cbBolt = startSketchOn(cbBore, face = START)\n |> circle(center = holePosition, radius = boltDiameter / 2, tag = $hole01)\n |> extrude(length = -stockThickness + counterboreDepth)\n\n return { }\n}\n\n// Place a counterbored hole at each bolt hole position\nmap(holePositions, f = counterbore)\n",
|
||||||
|
"main.kcl": "// Pillow Block Bearing\n// A bearing pillow block, also known as a plummer block or pillow block bearing, is a pedestal used to provide support for a rotating shaft with the help of compatible bearings and various accessories. Housing a bearing, the pillow block provides a secure and stable foundation that allows the shaft to rotate smoothly within its machinery setup. These components are essential in a wide range of mechanical systems and machinery, playing a key role in reducing friction and supporting radial and axial loads.\n\n// Set units\n@settings(defaultLengthUnit = in)\n\n// Import parts and parameters\nimport * from \"parameters.kcl\"\nimport \"ball-bearing.kcl\" as ballBearing\nimport \"block.kcl\" as block\n\n// Render each part\nballBearing\nblock\n",
|
||||||
|
"parameters.kcl": "// Global parameters for the pillow block bearing\n\n// Set units\n@settings(defaultLengthUnit = in)\n\n// Export parameters\nexport boltSpacingX = 5\nexport boltSpacingY = 3\nexport boltDiameter = 3 / 8\nexport counterboreDiameter = 3 / 4\nexport counterboreDepth = 3 / 16\nexport stockThickness = .5\nexport bearingBoreDiameter = 1 + 3 / 4\nexport bearingOuterDiameter = bearingBoreDiameter * 1.5\nexport sphereDia = (bearingOuterDiameter - bearingBoreDiameter) / 4\nexport chainWidth = sphereDia / 2\nexport chainThickness = sphereDia / 8\nexport linkDiameter = sphereDia / 4\n"
|
||||||
|
},
|
||||||
|
"expectedFiles": {
|
||||||
|
"ball-bearing.kcl": "// Pillow Block Bearing\n// The ball bearing for the pillow block bearing assembly\n\n// Set units\n@settings(defaultLengthUnit = in)\n\n// Import Parameters\nimport * from \"parameters.kcl\"\n\n// Create the sketch of one of the balls. The ball diameter is sized as a fraction of the difference between inner and outer radius of the bearing\nballsSketch = startSketchOn(offsetPlane(XY, offset = stockThickness / 2))\n |> startProfile(at = [bearingBoreDiameter / 2 + 0.1, 0.001])\n |> arc(angleEnd = 0, angleStart = 180, radius = sphereDia / 2)\n |> close()\n\n// Revolve the ball to make a sphere and pattern around the inside wall\nballs = revolve(ballsSketch, axis = X)\n |> patternCircular3d(\n arcDegrees = 360,\n axis = [0, 0, 1],\n center = [0, 0, 0],\n instances = 16,\n rotateDuplicates = true,\n )\n\n// Create the sketch for the chain around the balls\nchainSketch = startSketchOn(offsetPlane(XY, offset = stockThickness / 2))\n |> startProfile(at = [\n bearingBoreDiameter / 2 + 0.1 + sphereDia / 2 - (chainWidth / 2),\n 0.125 * sin(60deg)\n ])\n |> arc(angleEnd = 60, angleStart = 120, radius = sphereDia / 2)\n |> line(end = [0, chainThickness])\n |> line(end = [-chainWidth, 0])\n |> close()\n\n// Revolve the chain sketch\nchainHead = revolve(chainSketch, axis = X)\n |> patternCircular3d(\n arcDegrees = 360,\n axis = [0, 0, 1],\n center = [0, 0, 0],\n instances = 16,\n rotateDuplicates = true,\n )\n\n// Create the sketch for the links in between the chains\nlinkSketch = startSketchOn(XZ)\n |> circle(\n center = [\n bearingBoreDiameter / 2 + 0.1 + sphereDia / 2,\n stockThickness / 2\n ],\n radius = linkDiameter / 2,\n )\n\n// Create the walls of the bearing\nbearingBody = startSketchOn(XZ)\nbearingUpper = startProfile(\n bearingBody,\n at = [\n bearingOuterDiameter / 2 - .07,\n stockThickness\n ],\n )\n |> angledLine(angle = -91, length = 0.05)\n |> xLine(length = -(bearingOuterDiameter / 2 - (bearingBoreDiameter / 2)) + .145)\n |> yLine(endAbsolute = 0.105)\n |> xLine(length = -0.025)\n |> angledLine(angle = 91, endAbsoluteY = profileStartY(%))\n |> line(endAbsolute = [profileStartX(%), profileStartY(%)])\n |> close()\n |> revolve(angle = 360, axis = Y)\n |> appearance(%, color = \"#121212\")\n\nbearingLower = startProfile(bearingBody, at = [bearingBoreDiameter / 2, 0.025])\n |> xLine(length = 0.05)\n |> angledLine(angle = 75, length = 0.04, tag = $seg01)\n |> xLine(length = 0.05)\n |> angledLine(angle = -75, length = segLen(seg01))\n |> xLine(endAbsolute = bearingOuterDiameter / 2)\n |> yLine(length = stockThickness)\n |> xLine(length = -0.07)\n |> angledLine(angle = -91, endAbsoluteY = profileStartY(%) + .075)\n |> xLine(endAbsolute = profileStartX(%) + .05)\n |> angledLine(angle = 91, endAbsoluteY = stockThickness * 1.25)\n |> xLine(endAbsolute = profileStartX(%))\n |> line(endAbsolute = [profileStartX(%), profileStartY(%)])\n |> close()\n |> revolve(angle = 360, axis = Y)\n |> appearance(%, color = \"#ff0000\")\n\n// Revolve the link sketch\nrevolve(linkSketch, axis = Y, angle = 360 / 16)\n |> patternCircular3d(\n arcDegrees = 360,\n axis = [0, 0, 1],\n center = [0, 0, 0],\n instances = 16,\n rotateDuplicates = true,\n )\n",
|
||||||
|
"block.kcl": "// Pillow Block Bearing\n// The machined block for the pillow block bearing assembly. The block is dimensioned using the bolt pattern spacing, and each bolt hole includes a counterbore\n\n// Set units\n@settings(defaultLengthUnit = in)\n\n// Import Parameters\nimport * from \"parameters.kcl\"\n\n// Calculate the dimensions of the block using the specified bolt spacing. The size of the block can be defined by adding a multiple of the counterbore diameter to the bolt spacing\nblockLength = boltSpacingX + counterboreDiameter + boltDiameter\nblockWidth = boltSpacingY + counterboreDiameter + boltDiameter\n\n// Draw the base plate\nplateSketch = startSketchOn(XY)\n |> startProfile(at = [-blockLength / 2, -blockWidth / 2])\n |> angledLine(angle = 0, length = blockLength, tag = $rectangleSegmentA001)\n |> angledLine(angle = segAng(rectangleSegmentA001) + 90, length = blockWidth, tag = $rectangleSegmentB001)\n |> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001), tag = $rectangleSegmentC001)\n |> line(endAbsolute = [profileStartX(%), profileStartY(%)], tag = $rectangleSegmentD001)\n |> close()\n |> subtract2d(tool = circle(center = [0, 0], radius = bearingOuterDiameter / 2))\nplateBody = extrude(plateSketch, length = stockThickness)\n |> appearance(%, color = \"#1e62eb\")\n |> fillet(\n radius = boltDiameter * 1 / 3,\n tags = [\n getNextAdjacentEdge(rectangleSegmentB001),\n getNextAdjacentEdge(rectangleSegmentA001),\n getNextAdjacentEdge(rectangleSegmentC001),\n getNextAdjacentEdge(rectangleSegmentD001)\n ],\n )\n\n// Define hole positions\nholePositions = [\n [-boltSpacingX / 2, -boltSpacingY / 2],\n [-boltSpacingX / 2, boltSpacingY / 2],\n [boltSpacingX / 2, -boltSpacingY / 2],\n [boltSpacingX / 2, boltSpacingY / 2]\n]\n\n// Function to create a counterbored hole\nfn counterbore(@holePosition) {\n cbBore = startSketchOn(plateBody, face = END)\n |> circle(center = holePosition, radius = counterboreDiameter / 2)\n |> extrude(length = -counterboreDepth)\n cbBolt = startSketchOn(cbBore, face = START)\n |> circle(center = holePosition, radius = boltDiameter / 2, tag = $hole01)\n |> extrude(length = -stockThickness + counterboreDepth)\n\n return { }\n}\n\n// Place a counterbored hole at each bolt hole position\nmap(holePositions, f = counterbore)\n",
|
||||||
|
"main.kcl": "// Pillow Block Bearing\n// A bearing pillow block, also known as a plummer block or pillow block bearing, is a pedestal used to provide support for a rotating shaft with the help of compatible bearings and various accessories. Housing a bearing, the pillow block provides a secure and stable foundation that allows the shaft to rotate smoothly within its machinery setup. These components are essential in a wide range of mechanical systems and machinery, playing a key role in reducing friction and supporting radial and axial loads.\n\n// Set units\n@settings(defaultLengthUnit = in)\n\n// Import parts and parameters\nimport * from \"parameters.kcl\"\nimport \"ball-bearing.kcl\" as ballBearing\nimport \"block.kcl\" as block\n\n// Render each part\nballBearing\nblock\n",
|
||||||
|
"parameters.kcl": "// Global parameters for the pillow block bearing\n\n// Set units\n@settings(defaultLengthUnit = in)\n\n// Export parameters\nexport boltSpacingX = 5\nexport boltSpacingY = 3\nexport boltDiameter = 3 / 8\nexport counterboreDiameter = 3 / 4\nexport counterboreDepth = 3 / 16\nexport stockThickness = .5\nexport bearingBoreDiameter = 1 + 3 / 4\nexport bearingOuterDiameter = bearingBoreDiameter * 1.5\nexport sphereDia = (bearingOuterDiameter - bearingBoreDiameter) / 4\nexport chainWidth = sphereDia / 2\nexport chainThickness = sphereDia / 8\nexport linkDiameter = sphereDia / 4\n"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pattern this cylinder 6 times around the center of the flange, before subtracting it from the flange": {
|
||||||
|
"prompt": "pattern this cylinder 6 times around the center of the flange, before subtracting it from the flange",
|
||||||
|
"source_ranges": [
|
||||||
|
{
|
||||||
|
"prompt": "The users main selection is the wall of a general-sweep (that is an extrusion, revolve, sweep or loft).\nThe source range though is for the original segment before it was extruded, you can add a tag to that segment in order to refer to this wall, for example \"startSketchOn(someSweepVariable, face = segmentTag)\"\nBut it's also worth bearing in mind that the user may have intended to select the sweep itself, not this individual wall, see later source ranges for more context. about the sweep",
|
||||||
|
"range": {
|
||||||
|
"start": {
|
||||||
|
"line": 11,
|
||||||
|
"column": 22
|
||||||
|
},
|
||||||
|
"end": {
|
||||||
|
"line": 11,
|
||||||
|
"column": 89
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"file": "main.kcl"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt": "This is the sweep's source range from the user's main selection of the end cap.",
|
||||||
|
"range": {
|
||||||
|
"start": {
|
||||||
|
"line": 12,
|
||||||
|
"column": 24
|
||||||
|
},
|
||||||
|
"end": {
|
||||||
|
"line": 12,
|
||||||
|
"column": 66
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"file": "main.kcl"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"project_name": "test-project",
|
||||||
|
"files": {
|
||||||
|
"main.kcl": "flangeHolesR = 6\nflangeBodySketch = startSketchOn(XY)\nflangeBodyProfile = circle(flangeBodySketch, center = [0, 0], radius = 100)\nflangePlate = extrude(flangeBodyProfile, length = 5)\nhigherPlane = offsetPlane(XY, offset = 10)\ninnerBoreSketch = startSketchOn(higherPlane)\ninnerBoreProfile = circle(innerBoreSketch, center = [0, 0], radius = 49.28)\ninnerBoreCylinder = extrude(innerBoreProfile, length = -10)\nflangeBody = subtract([flangePlate], tools = [innerBoreCylinder])\nmountingHoleSketch = startSketchOn(higherPlane)\nmountingHoleProfile = circle(mountingHoleSketch, center = [75, 0], radius = flangeHolesR)\nmountingHoleCylinders = extrude(mountingHoleProfile, length = -30)\n"
|
||||||
|
},
|
||||||
|
"expectedFiles": {
|
||||||
|
"main.kcl": "flangeHolesR = 6\nflangeBodySketch = startSketchOn(XY)\nflangeBodyProfile = circle(flangeBodySketch, center = [0, 0], radius = 100)\nflangePlate = extrude(flangeBodyProfile, length = 5)\nhigherPlane = offsetPlane(XY, offset = 10)\ninnerBoreSketch = startSketchOn(higherPlane)\ninnerBoreProfile = circle(innerBoreSketch, center = [0, 0], radius = 49.28)\ninnerBoreCylinder = extrude(innerBoreProfile, length = -10)\nflangeBody = subtract([flangePlate], tools = [innerBoreCylinder])\nmountingHoleSketch = startSketchOn(higherPlane)\nmountingHoleProfile = circle(mountingHoleSketch, center = [75, 0], radius = flangeHolesR)\nmountingHoleCylinders = extrude(mountingHoleProfile, length = -30)\n |> patternCircular3d(instances = 6, axis = Z, center = [0, 0, 0])\nflange = subtract([flangeBody], tools = [mountingHoleCylinders])\n"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fillet these two edges please": {
|
||||||
|
"prompt": "fillet these two edges please",
|
||||||
|
"source_ranges": [
|
||||||
|
{
|
||||||
|
"prompt": "The users main selection is the edge of a general-sweep (that is an extrusion, revolve, sweep or loft).\nit is an opposite edge, in order to refer to this edge you should add a tag to the segment function in this source range,\nand then use the function getOppositeEdge\nSee later source ranges for more context. about the sweep",
|
||||||
|
"range": {
|
||||||
|
"start": {
|
||||||
|
"line": 3,
|
||||||
|
"column": 5
|
||||||
|
},
|
||||||
|
"end": {
|
||||||
|
"line": 3,
|
||||||
|
"column": 26
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"file": "main.kcl"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt": "This is the sweep's source range from the user's main selection of the end cap.",
|
||||||
|
"range": {
|
||||||
|
"start": {
|
||||||
|
"line": 11,
|
||||||
|
"column": 13
|
||||||
|
},
|
||||||
|
"end": {
|
||||||
|
"line": 11,
|
||||||
|
"column": 45
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"file": "main.kcl"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt": "The users main selection is the edge of a general-sweep (that is an extrusion, revolve, sweep or loft).\nit is an opposite edge, in order to refer to this edge you should add a tag to the segment function in this source range,\nand then use the function getOppositeEdge\nSee later source ranges for more context. about the sweep",
|
||||||
|
"range": {
|
||||||
|
"start": {
|
||||||
|
"line": 7,
|
||||||
|
"column": 5
|
||||||
|
},
|
||||||
|
"end": {
|
||||||
|
"line": 7,
|
||||||
|
"column": 27
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"file": "main.kcl"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prompt": "This is the sweep's source range from the user's main selection of the end cap.",
|
||||||
|
"range": {
|
||||||
|
"start": {
|
||||||
|
"line": 11,
|
||||||
|
"column": 13
|
||||||
|
},
|
||||||
|
"end": {
|
||||||
|
"line": 11,
|
||||||
|
"column": 45
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"file": "main.kcl"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"project_name": "test-project",
|
||||||
|
"files": {
|
||||||
|
"main.kcl": "sketch001 = startSketchOn(XZ)\nprofile001 = startProfile(sketch001, at = [18.47, 15.31])\n |> yLine(length = 28.26)\n |> line(end = [55.52, 21.93])\n |> tangentialArc(endAbsolute = [136.09, 36.87])\n |> yLine(length = -45.48)\n |> xLine(length = -13.76)\n |> yLine(length = 8.61)\n |> line(endAbsolute = [profileStartX(%), profileStartY(%)])\n |> close()\nextrude001 = extrude(profile001, length = 10)\n"
|
||||||
|
},
|
||||||
|
"expectedFiles": {
|
||||||
|
"main.kcl": "sketch001 = startSketchOn(XZ)\nprofile001 = startProfile(sketch001, at = [18.47, 15.31])\n |> yLine(length = 28.26, tag = $seg02)\n |> line(end = [55.52, 21.93], tag = $seg01)\n |> tangentialArc(endAbsolute = [136.09, 36.87])\n |> yLine(length = -45.48)\n |> xLine(length = -13.76, tag = $seg03)\n |> yLine(length = 8.61)\n |> line(endAbsolute = [profileStartX(%), profileStartY(%)])\n |> close()\nextrude001 = extrude(profile001, length = 10, tagEnd = $capEnd001)\n |> fillet(\n radius = 1,\n tags = [\n getCommonEdge(faces = [seg01, seg02]),\n getCommonEdge(faces = [seg03, capEnd001])\n ],\n )\n"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
859
src/machines/modifyWithTTC.test.ts
Normal file
859
src/machines/modifyWithTTC.test.ts
Normal file
@ -0,0 +1,859 @@
|
|||||||
|
import { engineCommandManager, kclManager } from '@src/lib/singletons'
|
||||||
|
import { VITE_KC_DEV_TOKEN } from '@src/env'
|
||||||
|
import { getModuleIdByFileName, isArray } from '@src/lib/utils'
|
||||||
|
import { vi, inject } from 'vitest'
|
||||||
|
import { assertParse } from '@src/lang/wasm'
|
||||||
|
import { initPromise } from '@src/lang/wasmUtils'
|
||||||
|
import { getCodeRefsByArtifactId } from '@src/lang/std/artifactGraph'
|
||||||
|
import path from 'path'
|
||||||
|
import fs from 'node:fs'
|
||||||
|
import type { Selections } from '@src/lib/selections'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This test is some what unique, and in fact it doesn't assert anything aside from capturing snapshots
|
||||||
|
* The snap shots are the request to the ML iteration endpoint, so that the ML team can use these in their own
|
||||||
|
* test harness.
|
||||||
|
* The reason why this is done at all is because the frontEnd has to be the source of truth because in the case of a user
|
||||||
|
* selecting something in the UI, the UI turns that artifactGraph selection into meta prompt that accompanies the user's prompt
|
||||||
|
*
|
||||||
|
* These are down as unit tests, because when a user selects something in the UI, that click resolves to an artifact in the artifactGraph
|
||||||
|
* So long as we're able to find the same artifact in the graph, we don't need the click.
|
||||||
|
* So far `artifactSearchSnippet` that has the file name, a searchString (some code in that file) and the artifact type ('wall', sweepEdge' etc) has
|
||||||
|
* been enough to find the artifact in the graph. That might need to change with more examples OR later if we have rock-solid stable ids
|
||||||
|
* We can possible just hard-code the ids and have that be reliable.
|
||||||
|
*
|
||||||
|
* The snapshot code is custom, instead of using Vitest's built-in snapshot functionality.
|
||||||
|
* This is purely because we want pure JSON to make this easy for the ML team to ingest
|
||||||
|
* It's been made to still work with the same `-u` flag, so it won't feel meaningfully different
|
||||||
|
* When they need to be updated.
|
||||||
|
*
|
||||||
|
* The way to add more examples is pushing new cases to `cases` array, you should be able
|
||||||
|
* to follow the patterns of other examples.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Custom JSON snapshot utilities
|
||||||
|
const SNAPSHOTS_DIR = path.join(__dirname, '__snapshots__')
|
||||||
|
const SNAPSHOTS_FILE = path.join(SNAPSHOTS_DIR, 'modifyWithTTC.test.json')
|
||||||
|
|
||||||
|
interface JsonSnapshots {
|
||||||
|
[testName: string]: any
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadJsonSnapshots(): JsonSnapshots {
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(SNAPSHOTS_FILE)) {
|
||||||
|
const content = fs.readFileSync(SNAPSHOTS_FILE, 'utf-8')
|
||||||
|
return JSON.parse(content)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to load JSON snapshots:', error)
|
||||||
|
}
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveJsonSnapshots(snapshots: JsonSnapshots): void {
|
||||||
|
try {
|
||||||
|
// Create directory if it doesn't exist
|
||||||
|
if (!fs.existsSync(SNAPSHOTS_DIR)) {
|
||||||
|
fs.mkdirSync(SNAPSHOTS_DIR, { recursive: true })
|
||||||
|
}
|
||||||
|
fs.writeFileSync(
|
||||||
|
SNAPSHOTS_FILE,
|
||||||
|
JSON.stringify(snapshots, null, 2),
|
||||||
|
'utf-8'
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to save JSON snapshots:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function expectJsonSnapshot(testName: string, data: any): void {
|
||||||
|
const snapshots = loadJsonSnapshots()
|
||||||
|
|
||||||
|
const serializedData = JSON.parse(JSON.stringify(data)) // Deep clone to remove any functions/symbols
|
||||||
|
|
||||||
|
// Try to detect update mode using inject
|
||||||
|
let isUpdateMode = false
|
||||||
|
try {
|
||||||
|
isUpdateMode = inject('vitest:updateSnapshots') || false
|
||||||
|
} catch {
|
||||||
|
// If inject fails, fall back to environment variable approach
|
||||||
|
isUpdateMode =
|
||||||
|
process.env.VITEST_UPDATE_SNAPSHOTS === 'true' ||
|
||||||
|
process.env.UPDATE_SNAPSHOTS === 'true'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isUpdateMode) {
|
||||||
|
// Update mode: save the new snapshot
|
||||||
|
snapshots[testName] = serializedData
|
||||||
|
saveJsonSnapshots(snapshots)
|
||||||
|
} else {
|
||||||
|
// Compare mode: check against existing snapshot
|
||||||
|
if (!(testName in snapshots)) {
|
||||||
|
throw new Error(`Snapshot missing for "${testName}". To update snapshots, run:
|
||||||
|
npm run test:unit -- -u modifyWithTTC.test.ts
|
||||||
|
Or set the UPDATE_SNAPSHOTS environment variable:
|
||||||
|
UPDATE_SNAPSHOTS=true npm run test:unit -- modifyWithTTC.test.ts`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const expected = snapshots[testName]
|
||||||
|
|
||||||
|
try {
|
||||||
|
expect(serializedData).toEqual(expected)
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Snapshot mismatch for "${testName}". To update snapshots, run:
|
||||||
|
npm run test:unit -- -u modifyWithTTC.test.ts
|
||||||
|
Or set the UPDATE_SNAPSHOTS environment variable:
|
||||||
|
UPDATE_SNAPSHOTS=true npm run test:unit -- modifyWithTTC.test.ts
|
||||||
|
|
||||||
|
Original error: ${error instanceof Error ? error.message : String(error)}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadSampleProject(fileName: string): {
|
||||||
|
[fileName: string]: string
|
||||||
|
} {
|
||||||
|
// public/kcl-samples/pillow-block-bearing/main.kcl
|
||||||
|
const projectPath = path.join('public', 'kcl-samples', fileName)
|
||||||
|
// load in all .kcl files in this directory using fs (sync)
|
||||||
|
const files: { [fileName: string]: string } = {}
|
||||||
|
const dir = path.dirname(projectPath)
|
||||||
|
const fileNames = fs.readdirSync(dir)
|
||||||
|
|
||||||
|
for (const file of fileNames) {
|
||||||
|
if (file.endsWith('.kcl')) {
|
||||||
|
const content = fs.readFileSync(path.join(dir, file), 'utf-8')
|
||||||
|
files[file] = content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return files
|
||||||
|
}
|
||||||
|
|
||||||
|
type TestCase = {
|
||||||
|
testName: string
|
||||||
|
prompt: string
|
||||||
|
inputFiles: { [fileName: string]: string }
|
||||||
|
expectedFiles: { [fileName: string]: string }
|
||||||
|
artifactSearchSnippet?: { fileName: string; content: string; type: string }[]
|
||||||
|
}
|
||||||
|
|
||||||
|
function createCaseData({
|
||||||
|
prompt,
|
||||||
|
inputFiles,
|
||||||
|
artifactSearchSnippet,
|
||||||
|
expectFilesCallBack,
|
||||||
|
testName,
|
||||||
|
}: Omit<TestCase, 'expectedFiles'> & {
|
||||||
|
expectFilesCallBack: (input: { fileName: string; content: string }) => string
|
||||||
|
}): TestCase {
|
||||||
|
return {
|
||||||
|
testName,
|
||||||
|
prompt,
|
||||||
|
inputFiles,
|
||||||
|
artifactSearchSnippet,
|
||||||
|
expectedFiles: Object.fromEntries(
|
||||||
|
Object.entries(inputFiles).map(([fileName, content]) => [
|
||||||
|
fileName,
|
||||||
|
expectFilesCallBack({ fileName, content }),
|
||||||
|
])
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const cases: TestCase[] = [
|
||||||
|
// // Add the static test case
|
||||||
|
createCaseData({
|
||||||
|
testName: 'change color',
|
||||||
|
prompt: 'make this neon green please, use #39FF14',
|
||||||
|
artifactSearchSnippet: [
|
||||||
|
{
|
||||||
|
content: 'line(end = [19.66, -116.4])',
|
||||||
|
fileName: 'main.kcl',
|
||||||
|
type: 'wall',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
expectFilesCallBack: ({ fileName, content }) => {
|
||||||
|
if (fileName !== 'main.kcl') return content
|
||||||
|
return content.replace(
|
||||||
|
'extrude001 = extrude(profile001, length = 200)',
|
||||||
|
`extrude001 = extrude(profile001, length = 200)
|
||||||
|
|> appearance(color = "#39FF14")`
|
||||||
|
)
|
||||||
|
},
|
||||||
|
inputFiles: {
|
||||||
|
'main.kcl': `import "b.kcl" as b
|
||||||
|
sketch001 = startSketchOn(XZ)
|
||||||
|
profile001 = startProfile(sketch001, at = [57.81, 250.51])
|
||||||
|
|> line(end = [121.13, 56.63], tag = $seg02)
|
||||||
|
|> line(end = [83.37, -34.61], tag = $seg01)
|
||||||
|
|> line(end = [19.66, -116.4])
|
||||||
|
|> line(end = [-221.8, -41.69])
|
||||||
|
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||||
|
|> close()
|
||||||
|
extrude001 = extrude(profile001, length = 200)
|
||||||
|
sketch002 = startSketchOn(XZ)
|
||||||
|
|> startProfile(at = [-73.64, -42.89])
|
||||||
|
|> xLine(length = 173.71)
|
||||||
|
|> line(end = [-22.12, -94.4])
|
||||||
|
|> line(end = [-22.12, -50.4])
|
||||||
|
|> line(end = [-22.12, -94.4])
|
||||||
|
|> line(end = [-22.12, -50.4])
|
||||||
|
|> xLine(length = -156.98)
|
||||||
|
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||||
|
|> close()
|
||||||
|
extrude002 = extrude(sketch002, length = 50)
|
||||||
|
b
|
||||||
|
`,
|
||||||
|
'b.kcl': `sketch003 = startSketchOn(XY)
|
||||||
|
|> startProfile(at = [52.92, 157.81])
|
||||||
|
|> angledLine(angle = 0, length = 176.4, tag = $rectangleSegmentA001)
|
||||||
|
|> angledLine(
|
||||||
|
angle = segAng(rectangleSegmentA001) - 90,
|
||||||
|
length = 53.4,
|
||||||
|
tag = $rectangleSegmentB001,
|
||||||
|
)
|
||||||
|
|> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001), tag = $rectangleSegmentC001)
|
||||||
|
|> line(end = [-22.12, -50.4])
|
||||||
|
|> line(end = [-22.12, -94.4])
|
||||||
|
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||||
|
|> close()
|
||||||
|
extrude(sketch003, length = 20)
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Load pillow block files and add as another test case
|
||||||
|
createCaseData({
|
||||||
|
testName: 'change color on imported file',
|
||||||
|
artifactSearchSnippet: [
|
||||||
|
{
|
||||||
|
fileName: 'ball-bearing.kcl',
|
||||||
|
content: 'yLine(length = stockThickness)',
|
||||||
|
type: 'wall',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
prompt: 'Change this to red please, #ff0000',
|
||||||
|
inputFiles: loadSampleProject('pillow-block-bearing/main.kcl'),
|
||||||
|
expectFilesCallBack: ({ fileName, content }) =>
|
||||||
|
fileName === 'ball-bearing.kcl'
|
||||||
|
? content.replace(
|
||||||
|
'appearance(%, color = "#f0f0f0")',
|
||||||
|
'appearance(%, color = "#ff0000")'
|
||||||
|
)
|
||||||
|
: content,
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
|
||||||
|
const patternHoleStarterCode: { [fileName: string]: string } = {
|
||||||
|
'main.kcl': `flangeHolesR = 6
|
||||||
|
flangeBodySketch = startSketchOn(XY)
|
||||||
|
flangeBodyProfile = circle(flangeBodySketch, center = [0, 0], radius = 100)
|
||||||
|
flangePlate = extrude(flangeBodyProfile, length = 5)
|
||||||
|
higherPlane = offsetPlane(XY, offset = 10)
|
||||||
|
innerBoreSketch = startSketchOn(higherPlane)
|
||||||
|
innerBoreProfile = circle(innerBoreSketch, center = [0, 0], radius = 49.28)
|
||||||
|
innerBoreCylinder = extrude(innerBoreProfile, length = -10)
|
||||||
|
flangeBody = subtract([flangePlate], tools = [innerBoreCylinder])
|
||||||
|
mountingHoleSketch = startSketchOn(higherPlane)
|
||||||
|
mountingHoleProfile = circle(mountingHoleSketch, center = [75, 0], radius = flangeHolesR)
|
||||||
|
mountingHoleCylinders = extrude(mountingHoleProfile, length = -30)
|
||||||
|
`,
|
||||||
|
}
|
||||||
|
|
||||||
|
cases.push(
|
||||||
|
createCaseData({
|
||||||
|
testName: 'pattern holes',
|
||||||
|
artifactSearchSnippet: [
|
||||||
|
{
|
||||||
|
fileName: 'main.kcl',
|
||||||
|
content:
|
||||||
|
'circle(mountingHoleSketch, center = [75, 0], radius = flangeHolesR)',
|
||||||
|
type: 'wall',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
prompt:
|
||||||
|
'pattern this cylinder 6 times around the center of the flange, before subtracting it from the flange',
|
||||||
|
inputFiles: patternHoleStarterCode,
|
||||||
|
expectFilesCallBack: ({ fileName, content }) => {
|
||||||
|
if (fileName !== 'main.kcl') return content
|
||||||
|
return content.replace(
|
||||||
|
'extrude(mountingHoleProfile, length = -30)',
|
||||||
|
`extrude(mountingHoleProfile, length = -30)
|
||||||
|
|> patternCircular3d(instances = 6, axis = Z, center = [0, 0, 0])
|
||||||
|
flange = subtract([flangeBody], tools = [mountingHoleCylinders])`
|
||||||
|
)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
const filletStarterCode: { [fileName: string]: string } = {
|
||||||
|
'main.kcl': `sketch001 = startSketchOn(XZ)
|
||||||
|
profile001 = startProfile(sketch001, at = [18.47, 15.31])
|
||||||
|
|> yLine(length = 28.26)
|
||||||
|
|> line(end = [55.52, 21.93])
|
||||||
|
|> tangentialArc(endAbsolute = [136.09, 36.87])
|
||||||
|
|> yLine(length = -45.48)
|
||||||
|
|> xLine(length = -13.76)
|
||||||
|
|> yLine(length = 8.61)
|
||||||
|
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||||
|
|> close()
|
||||||
|
extrude001 = extrude(profile001, length = 10)
|
||||||
|
`,
|
||||||
|
}
|
||||||
|
|
||||||
|
cases.push(
|
||||||
|
createCaseData({
|
||||||
|
testName: 'fillet shape',
|
||||||
|
artifactSearchSnippet: [
|
||||||
|
{
|
||||||
|
fileName: 'main.kcl',
|
||||||
|
content: 'yLine(length = 28.26)',
|
||||||
|
type: 'sweepEdge',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fileName: 'main.kcl',
|
||||||
|
content: 'xLine(length = -13.76)',
|
||||||
|
type: 'sweepEdge',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
prompt: 'fillet these two edges please',
|
||||||
|
inputFiles: filletStarterCode,
|
||||||
|
expectFilesCallBack: ({ fileName, content }) => {
|
||||||
|
if (fileName !== 'main.kcl') return content
|
||||||
|
let newContent = content.replace(
|
||||||
|
'extrude(profile001, length = 10)',
|
||||||
|
`extrude(profile001, length = 10, tagEnd = $capEnd001)
|
||||||
|
|> fillet(
|
||||||
|
radius = 1,
|
||||||
|
tags = [
|
||||||
|
getCommonEdge(faces = [seg01, seg02]),
|
||||||
|
getCommonEdge(faces = [seg03, capEnd001])
|
||||||
|
],
|
||||||
|
)`
|
||||||
|
)
|
||||||
|
newContent = newContent.replace(
|
||||||
|
'yLine(length = 28.26)',
|
||||||
|
'yLine(length = 28.26, tag = $seg02)'
|
||||||
|
)
|
||||||
|
newContent = newContent.replace(
|
||||||
|
'line(end = [55.52, 21.93])',
|
||||||
|
'line(end = [55.52, 21.93], tag = $seg01)'
|
||||||
|
)
|
||||||
|
newContent = newContent.replace(
|
||||||
|
'xLine(length = -13.76)',
|
||||||
|
'xLine(length = -13.76, tag = $seg03)'
|
||||||
|
)
|
||||||
|
return newContent
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
// Store original method to restore in afterAll
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await initPromise
|
||||||
|
|
||||||
|
// THESE TEST WILL FAIL without VITE_KC_DEV_TOKEN set in .env.development.local
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
engineCommandManager.start({
|
||||||
|
token: VITE_KC_DEV_TOKEN,
|
||||||
|
width: 256,
|
||||||
|
height: 256,
|
||||||
|
setMediaStream: () => {},
|
||||||
|
setIsStreamReady: () => {},
|
||||||
|
callbackOnEngineLiteConnect: () => {
|
||||||
|
resolve(true)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}, 30_000)
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
// Restore the original method
|
||||||
|
|
||||||
|
engineCommandManager.tearDown()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Define mock implementations that will be referenced in vi.mock calls
|
||||||
|
vi.mock('@src/components/SetHorVertDistanceModal', () => ({
|
||||||
|
createInfoModal: vi.fn(() => ({
|
||||||
|
open: vi.fn().mockResolvedValue({
|
||||||
|
value: '10',
|
||||||
|
segName: 'test',
|
||||||
|
valueNode: {},
|
||||||
|
newVariableInsertIndex: 0,
|
||||||
|
sign: 1,
|
||||||
|
}),
|
||||||
|
})),
|
||||||
|
GetInfoModal: vi.fn(),
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@src/components/SetAngleLengthModal', () => ({
|
||||||
|
createSetAngleLengthModal: vi.fn(() => ({
|
||||||
|
open: vi.fn().mockResolvedValue({
|
||||||
|
value: '45',
|
||||||
|
segName: 'test',
|
||||||
|
valueNode: {},
|
||||||
|
newVariableInsertIndex: 0,
|
||||||
|
sign: 1,
|
||||||
|
}),
|
||||||
|
})),
|
||||||
|
SetAngleLengthModal: vi.fn(),
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Create a utility to spy on fetch requests, similar to the Playwright fixture
|
||||||
|
interface CapturedRequest {
|
||||||
|
url: string
|
||||||
|
method: string
|
||||||
|
headers: Record<string, string>
|
||||||
|
body: {
|
||||||
|
[key: string]: any
|
||||||
|
files?: Record<string, string>
|
||||||
|
}
|
||||||
|
timestamp: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FetchSpyOptions {
|
||||||
|
captureAllRequests?: boolean
|
||||||
|
mockResponses?: Record<string, any>
|
||||||
|
}
|
||||||
|
|
||||||
|
function createFetchSpy(options: FetchSpyOptions = {}) {
|
||||||
|
const capturedRequests: CapturedRequest[] = []
|
||||||
|
const allFetchCalls: string[] = []
|
||||||
|
|
||||||
|
// Store test context for file mapping
|
||||||
|
let currentTestFiles: Record<string, string> = {}
|
||||||
|
|
||||||
|
// Create a mock fetch that handles the specific text-to-cad endpoints
|
||||||
|
const mockFetch = vi.fn(
|
||||||
|
async (url: string | URL | Request, init?: RequestInit) => {
|
||||||
|
const urlString = url.toString()
|
||||||
|
allFetchCalls.push(urlString)
|
||||||
|
|
||||||
|
// Capture requests based on options
|
||||||
|
const shouldCapture =
|
||||||
|
options.captureAllRequests ||
|
||||||
|
(urlString.includes('text-to-cad') && urlString.includes('iteration'))
|
||||||
|
|
||||||
|
if (shouldCapture) {
|
||||||
|
try {
|
||||||
|
const headers: Record<string, string> = {}
|
||||||
|
if (init?.headers) {
|
||||||
|
// Convert headers to a plain object
|
||||||
|
if (init.headers instanceof Headers) {
|
||||||
|
init.headers.forEach((value, key) => {
|
||||||
|
headers[key] = value
|
||||||
|
})
|
||||||
|
} else if (isArray(init.headers)) {
|
||||||
|
init.headers.forEach(([key, value]) => {
|
||||||
|
headers[key] = value
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Object.assign(headers, init.headers)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let requestBody: any = {}
|
||||||
|
let files: Record<string, string> = {}
|
||||||
|
|
||||||
|
// Parse multipart form data if present
|
||||||
|
if (init?.body instanceof FormData) {
|
||||||
|
// Extract the JSON body
|
||||||
|
const bodyData = init.body.get('body')
|
||||||
|
if (bodyData) {
|
||||||
|
requestBody = JSON.parse(bodyData.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract files and map them to correct names using test context
|
||||||
|
const fileContents: string[] = []
|
||||||
|
for (const [key, value] of init.body.entries()) {
|
||||||
|
if (key === 'files' && value instanceof File) {
|
||||||
|
const text = await value.text()
|
||||||
|
fileContents.push(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map files to their correct names based on test context
|
||||||
|
const testFileNames = Object.keys(currentTestFiles)
|
||||||
|
fileContents.forEach((content, index) => {
|
||||||
|
// Find matching file by content
|
||||||
|
const matchingFileName = testFileNames.find(
|
||||||
|
(fileName) => currentTestFiles[fileName] === content
|
||||||
|
)
|
||||||
|
|
||||||
|
if (matchingFileName) {
|
||||||
|
files[matchingFileName] = content
|
||||||
|
} else {
|
||||||
|
// Fallback if no exact match found
|
||||||
|
files[testFileNames[index] || `file-${index + 1}.kcl`] = content
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else if (init?.body && typeof init.body === 'string') {
|
||||||
|
// Parse multipart data manually like Playwright does
|
||||||
|
const postData = init.body
|
||||||
|
|
||||||
|
// Extract boundary from Content-Type header or find it in the data
|
||||||
|
const boundary = postData.match(
|
||||||
|
/------WebKitFormBoundary[^\r\n]*/
|
||||||
|
)?.[0]
|
||||||
|
if (boundary) {
|
||||||
|
const parts = postData
|
||||||
|
.split(boundary)
|
||||||
|
.filter((part) => part.trim())
|
||||||
|
|
||||||
|
for (const part of parts) {
|
||||||
|
// Skip the final boundary marker
|
||||||
|
if (part.startsWith('--')) continue
|
||||||
|
|
||||||
|
const nameMatch = part.match(/name="([^"]+)"/)
|
||||||
|
if (!nameMatch) continue
|
||||||
|
|
||||||
|
const name = nameMatch[1]
|
||||||
|
const content = part.split(/\r?\n\r?\n/)[1]?.trim()
|
||||||
|
if (!content) continue
|
||||||
|
|
||||||
|
if (name === 'body') {
|
||||||
|
requestBody = JSON.parse(content)
|
||||||
|
} else {
|
||||||
|
// This should be a file with the original filename as the key
|
||||||
|
files[name] = content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (init?.body) {
|
||||||
|
// Handle JSON body
|
||||||
|
try {
|
||||||
|
requestBody = JSON.parse(init.body.toString())
|
||||||
|
} catch {
|
||||||
|
requestBody = { raw: init.body.toString() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
capturedRequests.push({
|
||||||
|
url: urlString,
|
||||||
|
method: init?.method || 'GET',
|
||||||
|
headers,
|
||||||
|
body: {
|
||||||
|
...requestBody,
|
||||||
|
files,
|
||||||
|
},
|
||||||
|
timestamp: Date.now(),
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error capturing request:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for custom mock responses
|
||||||
|
if (options.mockResponses) {
|
||||||
|
for (const [pattern, response] of Object.entries(
|
||||||
|
options.mockResponses
|
||||||
|
)) {
|
||||||
|
if (urlString.includes(pattern)) {
|
||||||
|
return new Response(JSON.stringify(response), {
|
||||||
|
status: 200,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock text-to-cad iteration endpoint
|
||||||
|
if (
|
||||||
|
urlString.includes('text-to-cad') &&
|
||||||
|
urlString.includes('iteration')
|
||||||
|
) {
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
id: '550e8400-e29b-41d4-a716-446655440000',
|
||||||
|
status: 'queued',
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
status: 200,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock the async operations status endpoint
|
||||||
|
if (urlString.includes('async/operations/')) {
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
id: '550e8400-e29b-41d4-a716-446655440000',
|
||||||
|
status: 'completed',
|
||||||
|
outputs: {
|
||||||
|
'main.kcl': 'mocked KCL content with appearance applied',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
status: 200,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// For any other requests, return a basic successful response to avoid network errors
|
||||||
|
return new Response(JSON.stringify({ mocked: true }), {
|
||||||
|
status: 200,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
) as typeof global.fetch
|
||||||
|
|
||||||
|
// Use vi.stubGlobal for comprehensive mocking
|
||||||
|
vi.stubGlobal('fetch', mockFetch)
|
||||||
|
|
||||||
|
return {
|
||||||
|
getCapturedRequests: () => capturedRequests,
|
||||||
|
getRequestsMatching: (predicate: (req: CapturedRequest) => boolean) =>
|
||||||
|
capturedRequests.filter(predicate),
|
||||||
|
getTextToCadRequests: () =>
|
||||||
|
capturedRequests.filter((req) => req.url.includes('text-to-cad')),
|
||||||
|
getAllFetchCalls: () => allFetchCalls,
|
||||||
|
clearCapturedRequests: () => capturedRequests.splice(0),
|
||||||
|
setTestFiles: (files: Record<string, string>) => {
|
||||||
|
currentTestFiles = files
|
||||||
|
},
|
||||||
|
restore: () => {
|
||||||
|
vi.unstubAllGlobals()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add this function before the test cases
|
||||||
|
// Utility function to wait for a condition to be met
|
||||||
|
const waitForCondition = async (
|
||||||
|
condition: () => boolean,
|
||||||
|
timeout = 5000,
|
||||||
|
interval = 100
|
||||||
|
) => {
|
||||||
|
const startTime = Date.now()
|
||||||
|
|
||||||
|
while (Date.now() - startTime < timeout) {
|
||||||
|
try {
|
||||||
|
if (condition()) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Ignore errors, keep polling
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for the next interval
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, interval))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Last attempt before failing
|
||||||
|
return condition()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add this function before the test cases
|
||||||
|
|
||||||
|
// Utility function to set up a test project directory with KCL files
|
||||||
|
async function setupTestProjectWithImports(testFiles: Record<string, string>) {
|
||||||
|
const os = require('os')
|
||||||
|
const fs = require('fs/promises')
|
||||||
|
const path = require('path')
|
||||||
|
|
||||||
|
const testProjectDir = path.join(os.tmpdir(), `kcl-test-${Date.now()}`)
|
||||||
|
|
||||||
|
// Set up test files
|
||||||
|
await fs.mkdir(testProjectDir, { recursive: true })
|
||||||
|
|
||||||
|
// Write all the test files
|
||||||
|
for (const [filename, content] of Object.entries(testFiles)) {
|
||||||
|
await fs.writeFile(path.join(testProjectDir, filename), content)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure the FileSystemManager to use our test directory
|
||||||
|
const { fileSystemManager } = await import('@src/lang/std/fileSystemManager')
|
||||||
|
fileSystemManager.dir = testProjectDir
|
||||||
|
|
||||||
|
return {
|
||||||
|
projectDir: testProjectDir,
|
||||||
|
cleanup: async () => {
|
||||||
|
try {
|
||||||
|
await fs.rm(testProjectDir, { recursive: true })
|
||||||
|
} catch {
|
||||||
|
// Ignore cleanup errors
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('When prompting modify with TTC, prompt:', () => {
|
||||||
|
cases.forEach(
|
||||||
|
({ prompt, inputFiles, artifactSearchSnippet, expectedFiles }) => {
|
||||||
|
it(`${prompt}`, async () => {
|
||||||
|
const mainFile = inputFiles['main.kcl']
|
||||||
|
|
||||||
|
const { cleanup } = await setupTestProjectWithImports(inputFiles)
|
||||||
|
|
||||||
|
// Set up fetch spy to capture requests
|
||||||
|
const fetchSpy = createFetchSpy()
|
||||||
|
|
||||||
|
// Set the test files for proper filename mapping
|
||||||
|
fetchSpy.setTestFiles(inputFiles)
|
||||||
|
|
||||||
|
// Set up mock token for authentication
|
||||||
|
const mockToken = 'test-token-123'
|
||||||
|
localStorage.setItem('TOKEN_PERSIST_KEY', mockToken)
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Parse and execute the main file with imports
|
||||||
|
const ast = assertParse(mainFile)
|
||||||
|
|
||||||
|
// Execute the AST - the fileSystemManager.dir will be used for import resolution
|
||||||
|
await kclManager.executeAst({ ast })
|
||||||
|
|
||||||
|
expect(kclManager.errors).toEqual([])
|
||||||
|
|
||||||
|
let selections: Selections = {
|
||||||
|
graphSelections: [],
|
||||||
|
otherSelections: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
if (artifactSearchSnippet) {
|
||||||
|
artifactSearchSnippet.forEach((snippet) => {
|
||||||
|
let moduleId = getModuleIdByFileName(
|
||||||
|
snippet.fileName,
|
||||||
|
kclManager.execState.filenames
|
||||||
|
)
|
||||||
|
if (snippet.fileName === 'main.kcl') {
|
||||||
|
moduleId = 0
|
||||||
|
}
|
||||||
|
const moduleContent = inputFiles[snippet.fileName]
|
||||||
|
if (moduleId === -1) {
|
||||||
|
throw new Error(
|
||||||
|
`Module ID not found for file: ${snippet.fileName}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (!moduleContent) {
|
||||||
|
throw new Error(
|
||||||
|
`Module content not found for file: ${snippet.fileName}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const indexOfInterest = moduleContent.indexOf(snippet.content)
|
||||||
|
|
||||||
|
const artifacts = [...kclManager.artifactGraph].filter(
|
||||||
|
([id, artifact]) => {
|
||||||
|
const codeRefs = getCodeRefsByArtifactId(
|
||||||
|
id,
|
||||||
|
kclManager.artifactGraph
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
artifact?.type === snippet.type &&
|
||||||
|
codeRefs &&
|
||||||
|
codeRefs.find((ref) => {
|
||||||
|
return (
|
||||||
|
ref.range[0] <= indexOfInterest &&
|
||||||
|
ref.range[1] >= indexOfInterest &&
|
||||||
|
ref.range[2] === moduleId
|
||||||
|
)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
const artifact = artifacts?.[0]?.[1]
|
||||||
|
|
||||||
|
if (!artifact) {
|
||||||
|
throw new Error('Artifact not found')
|
||||||
|
}
|
||||||
|
const codeRef = (getCodeRefsByArtifactId(
|
||||||
|
artifact.id,
|
||||||
|
kclManager.artifactGraph
|
||||||
|
) || [])[0]
|
||||||
|
if (!codeRef) {
|
||||||
|
throw new Error('Code reference not found for the artifact')
|
||||||
|
}
|
||||||
|
|
||||||
|
selections.graphSelections.push({
|
||||||
|
artifact,
|
||||||
|
codeRef,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// Test that we can work with the imported content
|
||||||
|
|
||||||
|
// Test direct call to promptToEditFlow instead of going through state machine
|
||||||
|
const { promptToEditFlow } = await import('@src/lib/promptToEdit')
|
||||||
|
|
||||||
|
// Create project files that match what the state machine would create
|
||||||
|
const projectFiles = Object.entries(inputFiles).map(
|
||||||
|
([filename, content]) => ({
|
||||||
|
type: 'kcl' as const,
|
||||||
|
relPath: filename,
|
||||||
|
absPath: filename,
|
||||||
|
fileContents: content,
|
||||||
|
execStateFileNamesIndex: Number(
|
||||||
|
Object.entries(kclManager.execState.filenames).find(
|
||||||
|
([_, value]) =>
|
||||||
|
value && value.type === 'Local' && value.value === filename
|
||||||
|
)?.[0] || 0
|
||||||
|
),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
// Call promptToEditFlow directly
|
||||||
|
const resultPromise = promptToEditFlow({
|
||||||
|
prompt,
|
||||||
|
selections,
|
||||||
|
projectFiles,
|
||||||
|
token: mockToken,
|
||||||
|
artifactGraph: kclManager.artifactGraph,
|
||||||
|
projectName: 'test-project',
|
||||||
|
filePath: 'main.kcl',
|
||||||
|
})
|
||||||
|
|
||||||
|
// Wait for the request to be made
|
||||||
|
await waitForCondition(
|
||||||
|
() => {
|
||||||
|
const requests = fetchSpy.getCapturedRequests()
|
||||||
|
return requests.length > 0
|
||||||
|
},
|
||||||
|
10000,
|
||||||
|
500
|
||||||
|
)
|
||||||
|
|
||||||
|
// Get and verify the captured request
|
||||||
|
const capturedRequests = fetchSpy.getCapturedRequests()
|
||||||
|
fetchSpy.getAllFetchCalls()
|
||||||
|
|
||||||
|
if (capturedRequests.length === 0) {
|
||||||
|
console.log(
|
||||||
|
'No text-to-cad requests were captured. This might indicate an error in the flow.'
|
||||||
|
)
|
||||||
|
expect(capturedRequests).toHaveLength(1) // This will fail and show what was captured
|
||||||
|
} else {
|
||||||
|
const request = capturedRequests[0]
|
||||||
|
const { kcl_version, ...body } = request.body // peel off and discard kcl_version
|
||||||
|
const textToCadPayload = {
|
||||||
|
...body,
|
||||||
|
// Normalize file names to make snapshots deterministic
|
||||||
|
files: request.body.files,
|
||||||
|
expectedFiles,
|
||||||
|
}
|
||||||
|
// delete textToCadPayload.kcl_version
|
||||||
|
|
||||||
|
// Use custom JSON snapshot instead of Vitest's default format
|
||||||
|
expectJsonSnapshot(prompt, textToCadPayload)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for the promise to resolve or reject
|
||||||
|
try {
|
||||||
|
await resultPromise
|
||||||
|
} catch {
|
||||||
|
// most likely get a auth error here for TTC, we don't actually care about the response.
|
||||||
|
// just capturing the request
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
fetchSpy.restore()
|
||||||
|
localStorage.removeItem('TOKEN_PERSIST_KEY')
|
||||||
|
await cleanup()
|
||||||
|
}
|
||||||
|
}, 20_000) // Increase timeout to 20 seconds
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
17
src/test-setup/global-setup.ts
Normal file
17
src/test-setup/global-setup.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import type { GlobalSetupContext } from 'vitest/node'
|
||||||
|
|
||||||
|
export default function setup({ provide, config }: GlobalSetupContext) {
|
||||||
|
// Check if snapshot update mode is enabled
|
||||||
|
// Only update existing snapshots when mode is 'all' (triggered by -u flag)
|
||||||
|
// 'new' mode only creates snapshots for tests that don't have any, but doesn't update existing ones
|
||||||
|
const isUpdateSnapshots = config.snapshotOptions.updateSnapshot === 'all'
|
||||||
|
|
||||||
|
// Provide the update flag to tests
|
||||||
|
provide('vitest:updateSnapshots', isUpdateSnapshots)
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'vitest' {
|
||||||
|
export interface ProvidedContext {
|
||||||
|
'vitest:updateSnapshots': boolean
|
||||||
|
}
|
||||||
|
}
|
22
vitest.config.ts
Normal file
22
vitest.config.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { defineConfig } from 'vitest/config'
|
||||||
|
import tsconfigPaths from 'vite-tsconfig-paths'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [tsconfigPaths()],
|
||||||
|
test: {
|
||||||
|
globals: true,
|
||||||
|
globalSetup: './src/test-setup/global-setup.ts',
|
||||||
|
environment: 'happy-dom',
|
||||||
|
setupFiles: ['./src/setupTests.ts'],
|
||||||
|
include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
|
||||||
|
exclude: [
|
||||||
|
'**/node_modules/**',
|
||||||
|
'**/dist/**',
|
||||||
|
'**/cypress/**',
|
||||||
|
'**/.{idea,git,cache,output,temp}/**',
|
||||||
|
],
|
||||||
|
deps: {
|
||||||
|
external: [/playwright/],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
Reference in New Issue
Block a user