@akanjs/devkit 3.0.0-beta.13 → 3.0.0-beta.15
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.
- package/README.md +1 -0
- package/cloud/cloudApi.ts +17 -4
- package/cloud/constants.ts +0 -3
- package/cloud/globalConfig.ts +1 -9
- package/codeAgent/agent/AkanAuthStorageBackend.ts +105 -0
- package/codeAgent/agent/AkanCodeServices.ts +104 -0
- package/codeAgent/agent/CodeAgent.ts +543 -0
- package/codeAgent/agent/CodeAgentAsks.ts +87 -0
- package/codeAgent/agent/CodeAgentEventMapper.ts +260 -0
- package/codeAgent/agent/CodeAgentGate.ts +119 -0
- package/codeAgent/agent/CodeAgentRpcHost.ts +96 -0
- package/codeAgent/agent/CodeAgentStreamPrinter.ts +93 -0
- package/codeAgent/agent/CodeAgentUi.ts +52 -0
- package/codeAgent/agent/CodeMailbox.ts +175 -0
- package/codeAgent/agent/CodeSessionFork.ts +71 -0
- package/codeAgent/agent/CodeSessionIndex.ts +104 -0
- package/codeAgent/agent/SubagentPool.ts +173 -0
- package/codeAgent/agent/akanCodeModel.ts +81 -0
- package/codeAgent/agent/akanCodePaths.ts +31 -0
- package/codeAgent/agent/akanSystemPrompt.ts +49 -0
- package/codeAgent/agent/codeAgentContract.test.ts +529 -0
- package/codeAgent/agent/codeMailbox.test.ts +80 -0
- package/codeAgent/agent/codeSessionFork.test.ts +60 -0
- package/codeAgent/agent/codeSessionIndex.test.ts +72 -0
- package/codeAgent/agent/consoleToStderr.ts +18 -0
- package/codeAgent/feedback/DevLogFeedback.ts +97 -0
- package/codeAgent/feedback/PreviewChrome.ts +106 -0
- package/codeAgent/feedback/PreviewFeedback.ts +53 -0
- package/codeAgent/feedback/PreviewView.ts +107 -0
- package/codeAgent/feedback/TurnFeedback.ts +113 -0
- package/codeAgent/feedback/VerifyFeedback.ts +46 -0
- package/codeAgent/feedback/previewChrome.test.ts +73 -0
- package/codeAgent/index.ts +43 -0
- package/codeAgent/skills/akan-debug/SKILL.md +42 -0
- package/codeAgent/skills/akan-deps/SKILL.md +50 -0
- package/codeAgent/skills/akan-err/SKILL.md +39 -0
- package/codeAgent/skills/akan-module/SKILL.md +45 -0
- package/codeAgent/skills/akan-scaffold/SKILL.md +76 -0
- package/codeAgent/skills/akan-scalar/SKILL.md +52 -0
- package/codeAgent/skills/akan-service/SKILL.md +34 -0
- package/codeAgent/skills/akan-store/SKILL.md +63 -0
- package/codeAgent/skills/akan-ui/SKILL.md +63 -0
- package/codeAgent/skills/akan-validate/SKILL.md +44 -0
- package/codeAgent/tools/AkanCli.ts +62 -0
- package/codeAgent/tools/AkanCodePlugins.ts +135 -0
- package/codeAgent/tools/AkanEditScope.ts +58 -0
- package/codeAgent/tools/AkanEnvKeys.ts +56 -0
- package/codeAgent/tools/AkanToolPack.ts +114 -0
- package/codeAgent/tools/AkanVerifier.ts +55 -0
- package/codeAgent/tools/MailToolPack.ts +85 -0
- package/codeAgent/tools/McpClient.ts +162 -0
- package/codeAgent/tools/McpToolPack.ts +134 -0
- package/codeAgent/tools/SessionToolPack.ts +101 -0
- package/codeAgent/tools/WebToolPack.ts +153 -0
- package/codeAgent/tools/akanCodePlugins.test.ts +476 -0
- package/commandDecorators/command.ts +1 -1
- package/incrementalBuilder/incrementalBuilder.proc.ts +1 -1
- package/index.ts +3 -2
- package/package.json +5 -5
- package/stripAnsi.ts +3 -0
- package/testHelpers.ts +179 -0
- package/abstractCompactor.ts +0 -112
- package/aiEditor.test.ts +0 -60
- package/aiEditor.ts +0 -387
package/testHelpers.ts
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { AppExecutor, LibExecutor, ModuleExecutor, PkgExecutor, WorkspaceExecutor } from "@akanjs/devkit/executors";
|
|
5
|
+
export interface CallRecord {
|
|
6
|
+
name: string;
|
|
7
|
+
args: unknown[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const createCallRecorder = () => {
|
|
11
|
+
const calls: CallRecord[] = [];
|
|
12
|
+
return {
|
|
13
|
+
calls,
|
|
14
|
+
/**
|
|
15
|
+
* `Returns` lets one recorder stand in for methods that return a value. Nothing is produced — the
|
|
16
|
+
* stub resolves to `undefined`, so use it only where the code under test does not read the result;
|
|
17
|
+
* a test that needs the value should return it from its own closure instead.
|
|
18
|
+
*/
|
|
19
|
+
record<Returns = void>(name: string, ...args: unknown[]): Returns {
|
|
20
|
+
calls.push({ name, args });
|
|
21
|
+
return undefined as unknown as Returns;
|
|
22
|
+
},
|
|
23
|
+
names() {
|
|
24
|
+
return calls.map((call) => call.name);
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const createFakeSpinner = (recorder = createCallRecorder()) => ({
|
|
30
|
+
succeed: (message?: string) => recorder.record("spinner.succeed", message),
|
|
31
|
+
fail: (message?: string) => recorder.record("spinner.fail", message),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
export const createFakeExecutor = <Extra extends object = object>(
|
|
35
|
+
name: string,
|
|
36
|
+
extra: Extra = {} as Extra,
|
|
37
|
+
recorder = createCallRecorder(),
|
|
38
|
+
) =>
|
|
39
|
+
({
|
|
40
|
+
name,
|
|
41
|
+
cwdPath: `/workspace/${name}`,
|
|
42
|
+
workspaceRoot: "/workspace",
|
|
43
|
+
spinning: (message: string) => {
|
|
44
|
+
recorder.record(`${name}.spinning`, message);
|
|
45
|
+
return createFakeSpinner(recorder);
|
|
46
|
+
},
|
|
47
|
+
spawn: async (...args: unknown[]) => {
|
|
48
|
+
recorder.record(`${name}.spawn`, ...args);
|
|
49
|
+
return "";
|
|
50
|
+
},
|
|
51
|
+
exec: async (...args: unknown[]) => {
|
|
52
|
+
recorder.record(`${name}.exec`, ...args);
|
|
53
|
+
return "";
|
|
54
|
+
},
|
|
55
|
+
scan: async (...args: unknown[]) => {
|
|
56
|
+
recorder.record(`${name}.scan`, ...args);
|
|
57
|
+
return { name };
|
|
58
|
+
},
|
|
59
|
+
scanSync: async (...args: unknown[]) => {
|
|
60
|
+
recorder.record(`${name}.scanSync`, ...args);
|
|
61
|
+
return { name };
|
|
62
|
+
},
|
|
63
|
+
...extra,
|
|
64
|
+
}) as Extra & {
|
|
65
|
+
name: string;
|
|
66
|
+
cwdPath: string;
|
|
67
|
+
workspaceRoot: string;
|
|
68
|
+
spinning: (message: string) => ReturnType<typeof createFakeSpinner>;
|
|
69
|
+
spawn: (...args: unknown[]) => Promise<string>;
|
|
70
|
+
exec: (...args: unknown[]) => Promise<string>;
|
|
71
|
+
scan: (...args: unknown[]) => Promise<{ name: string }>;
|
|
72
|
+
scanSync: (...args: unknown[]) => Promise<{ name: string }>;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export const makeCliTempWorkspace = async () => {
|
|
76
|
+
const root = await mkdtemp(path.join(os.tmpdir(), "akan-cli-"));
|
|
77
|
+
await mkdir(root, { recursive: true });
|
|
78
|
+
await writeText(path.join(root, ".gitignore"), "");
|
|
79
|
+
await writeText(
|
|
80
|
+
path.join(root, ".env"),
|
|
81
|
+
["AKAN_PUBLIC_REPO_NAME=repo", "AKAN_PUBLIC_SERVE_DOMAIN=localhost", "AKAN_PUBLIC_ENV=local", ""].join("\n"),
|
|
82
|
+
);
|
|
83
|
+
const workspace = new WorkspaceExecutor({ workspaceRoot: root, repoName: "repo" });
|
|
84
|
+
return { root, workspace };
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export const cleanupCliTempWorkspace = async (root: string) => {
|
|
88
|
+
await rm(root, { recursive: true, force: true });
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export const writeText = async (filePath: string, content: string) => {
|
|
92
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
93
|
+
await writeFile(filePath, content);
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export const writeJson = async (filePath: string, value: object) => {
|
|
97
|
+
await writeText(filePath, `${JSON.stringify(value, null, 2)}\n`);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export const createTempApp = async (appName = "demo") => {
|
|
101
|
+
const { root, workspace } = await makeCliTempWorkspace();
|
|
102
|
+
await writeJson(path.join(root, "package.json"), {
|
|
103
|
+
name: "repo",
|
|
104
|
+
version: "1.0.0",
|
|
105
|
+
description: "repo",
|
|
106
|
+
dependencies: {
|
|
107
|
+
react: "19.0.0",
|
|
108
|
+
"react-dom": "19.0.0",
|
|
109
|
+
"react-server-dom-webpack": "19.0.0",
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
await writeJson(path.join(root, "tsconfig.json"), {
|
|
113
|
+
compilerOptions: { target: "ESNext", paths: {} },
|
|
114
|
+
references: [],
|
|
115
|
+
});
|
|
116
|
+
await writeJson(path.join(root, "apps", appName, "tsconfig.json"), {
|
|
117
|
+
compilerOptions: { target: "ESNext", paths: {} },
|
|
118
|
+
});
|
|
119
|
+
await writeJson(path.join(root, "apps", appName, "package.json"), {
|
|
120
|
+
name: appName,
|
|
121
|
+
version: "1.0.0",
|
|
122
|
+
description: appName,
|
|
123
|
+
dependencies: {},
|
|
124
|
+
devDependencies: {},
|
|
125
|
+
});
|
|
126
|
+
await writeText(path.join(root, "apps", appName, "akan.config.ts"), "export default {};\n");
|
|
127
|
+
await mkdir(path.join(root, "apps", appName, "lib", "__scalar"), { recursive: true });
|
|
128
|
+
const app = AppExecutor.from(workspace, appName);
|
|
129
|
+
return { root, workspace, app };
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
export const createTempLib = async (libName = "shared") => {
|
|
133
|
+
const { root, workspace } = await makeCliTempWorkspace();
|
|
134
|
+
await writeJson(path.join(root, "package.json"), {
|
|
135
|
+
name: "repo",
|
|
136
|
+
version: "1.0.0",
|
|
137
|
+
description: "repo",
|
|
138
|
+
dependencies: {},
|
|
139
|
+
devDependencies: {},
|
|
140
|
+
});
|
|
141
|
+
await writeJson(path.join(root, "tsconfig.json"), {
|
|
142
|
+
compilerOptions: { target: "ESNext", paths: {} },
|
|
143
|
+
references: [],
|
|
144
|
+
});
|
|
145
|
+
const lib = LibExecutor.from(workspace, libName);
|
|
146
|
+
return { root, workspace, lib };
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
export const createTempPackage = async (pkgName = "@sample/tool") => {
|
|
150
|
+
const { root, workspace } = await makeCliTempWorkspace();
|
|
151
|
+
await writeJson(path.join(root, "package.json"), {
|
|
152
|
+
name: "repo",
|
|
153
|
+
version: "1.0.0",
|
|
154
|
+
description: "repo",
|
|
155
|
+
dependencies: { lodash: "4.0.0" },
|
|
156
|
+
devDependencies: { typescript: "6.0.0" },
|
|
157
|
+
});
|
|
158
|
+
await writeJson(path.join(root, "tsconfig.json"), {
|
|
159
|
+
compilerOptions: { target: "ESNext", paths: {} },
|
|
160
|
+
});
|
|
161
|
+
await writeJson(path.join(root, "pkgs", pkgName, "package.json"), {
|
|
162
|
+
name: pkgName,
|
|
163
|
+
version: "0.1.0",
|
|
164
|
+
description: "tool",
|
|
165
|
+
exports: {},
|
|
166
|
+
});
|
|
167
|
+
await writeJson(path.join(root, "pkgs", pkgName, "tsconfig.json"), {
|
|
168
|
+
compilerOptions: { target: "ESNext", paths: {} },
|
|
169
|
+
});
|
|
170
|
+
await writeText(path.join(root, "pkgs", pkgName, "index.ts"), 'import "lodash";\nexport const value = 1;\n');
|
|
171
|
+
const pkg = PkgExecutor.from(workspace, pkgName);
|
|
172
|
+
return { root, workspace, pkg };
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
export const createTempModule = async (moduleName = "post") => {
|
|
176
|
+
const { root, workspace, app } = await createTempApp("demo");
|
|
177
|
+
const module = ModuleExecutor.from(app, moduleName);
|
|
178
|
+
return { root, workspace, app, module };
|
|
179
|
+
};
|
package/abstractCompactor.ts
DELETED
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
import { Logger } from "akanjs/common";
|
|
2
|
-
import chalk from "chalk";
|
|
3
|
-
import { AbstractDoc, type AbstractKind } from "./abstractDoc";
|
|
4
|
-
import { AiSession } from "./aiEditor";
|
|
5
|
-
import type { SysExecutor } from "./executors";
|
|
6
|
-
|
|
7
|
-
export interface AbstractCompactOptions {
|
|
8
|
-
module?: string | null;
|
|
9
|
-
minLines?: number;
|
|
10
|
-
interactive?: boolean;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export interface AbstractCompactReport {
|
|
14
|
-
path: string;
|
|
15
|
-
beforeLines: number;
|
|
16
|
-
afterLines: number;
|
|
17
|
-
status: "compacted" | "unchanged" | "failed";
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/** Rewrites bloated `*.abstract.md` files down to the invariants their source files cannot show. */
|
|
21
|
-
export class AbstractCompactor {
|
|
22
|
-
static readonly #reviewRequest = `Review the compacted file you just wrote against the original.
|
|
23
|
-
- Did you drop an invariant, a security reason, or a workflow step that the source files cannot show by themselves? Restore it.
|
|
24
|
-
- Did you keep anything that only restates code, scaffold wording, or history? Remove it.
|
|
25
|
-
- Is it in the same language as the original, within the line budget, and one short line per bullet?
|
|
26
|
-
Respond with exactly one \`\`\`markdown code block holding the corrected file and nothing else. If nothing needs to change, repeat the file unchanged.`;
|
|
27
|
-
|
|
28
|
-
#sys: SysExecutor;
|
|
29
|
-
#minLines: number;
|
|
30
|
-
#interactive: boolean;
|
|
31
|
-
constructor(
|
|
32
|
-
sys: SysExecutor,
|
|
33
|
-
{ minLines = AbstractDoc.compactMinLines, interactive = false }: AbstractCompactOptions = {},
|
|
34
|
-
) {
|
|
35
|
-
this.#sys = sys;
|
|
36
|
-
this.#minLines = minLines;
|
|
37
|
-
this.#interactive = interactive;
|
|
38
|
-
}
|
|
39
|
-
async compactAll({ module }: { module?: string | null } = {}) {
|
|
40
|
-
const docs = await AbstractDoc.findAll(this.#sys, { module });
|
|
41
|
-
const targets = docs.filter((doc) => doc.lineCount > this.#minLines);
|
|
42
|
-
const reports: AbstractCompactReport[] = [];
|
|
43
|
-
// One file at a time: each doc gets its own AI session, and parallel runs would interleave the
|
|
44
|
-
// streamed response with each other and with the interactive confirm prompt.
|
|
45
|
-
for (const doc of targets) reports.push(await this.compact(doc));
|
|
46
|
-
return { scanned: docs.length, reports };
|
|
47
|
-
}
|
|
48
|
-
async compact(doc: AbstractDoc): Promise<AbstractCompactReport> {
|
|
49
|
-
const session = new AiSession("compactAbstract", {
|
|
50
|
-
workspace: this.#sys.workspace,
|
|
51
|
-
cacheKey: `${this.#sys.name}-${doc.moduleName}`,
|
|
52
|
-
});
|
|
53
|
-
const approve = !this.#interactive;
|
|
54
|
-
const compacted = await session.editMarkdown(this.#request(doc), { approve });
|
|
55
|
-
const reviewed = await session.editMarkdown(AbstractCompactor.#reviewRequest, { approve });
|
|
56
|
-
// The review answers in prose when it finds nothing to fix, so the first compaction is the fallback.
|
|
57
|
-
const next = [reviewed, compacted]
|
|
58
|
-
.map((candidate) => candidate.trim())
|
|
59
|
-
.find((candidate) => doc.canReplaceWith(candidate));
|
|
60
|
-
const beforeLines = doc.lineCount;
|
|
61
|
-
if (!next) {
|
|
62
|
-
Logger.rawLog(chalk.yellow(`${doc.path}: the editor returned no shorter abstract, keeping the current file`));
|
|
63
|
-
return { path: doc.path, beforeLines, afterLines: beforeLines, status: "failed" };
|
|
64
|
-
}
|
|
65
|
-
if (next === doc.content.trim())
|
|
66
|
-
return { path: doc.path, beforeLines, afterLines: beforeLines, status: "unchanged" };
|
|
67
|
-
const content = `${next}\n`;
|
|
68
|
-
await this.#sys.writeFile(doc.path, content);
|
|
69
|
-
return { path: doc.path, beforeLines, afterLines: AbstractDoc.lineCountOf(content), status: "compacted" };
|
|
70
|
-
}
|
|
71
|
-
#targetShape(kind: AbstractKind) {
|
|
72
|
-
if (kind === "other")
|
|
73
|
-
return " - keep the headings that still carry information and drop the rest; do not impose a new structure";
|
|
74
|
-
return [
|
|
75
|
-
" - `# <name> Abstract` title line",
|
|
76
|
-
" - one declarative sentence naming what this module owns",
|
|
77
|
-
" - `## Rules` — two to five bullets, each an invariant the code cannot show by itself",
|
|
78
|
-
" - optional `## Workflow` — the lifecycle as an arrow chain (`draft -> signed -> active`) or a few bullets",
|
|
79
|
-
].join("\n");
|
|
80
|
-
}
|
|
81
|
-
#request(doc: AbstractDoc) {
|
|
82
|
-
return `You are compacting an Akan.js abstract file.
|
|
83
|
-
|
|
84
|
-
An abstract is the summary a coding agent reads before changing a module. Repeated edits have made this one
|
|
85
|
-
bloated, and it has to shrink back to what the source files cannot show by themselves.
|
|
86
|
-
|
|
87
|
-
# Target file
|
|
88
|
-
${this.#sys.type}s/${this.#sys.name}/${doc.path} (${doc.kind} module "${doc.moduleName}", ${doc.lineCount} lines)
|
|
89
|
-
|
|
90
|
-
# Source files in the same folder
|
|
91
|
-
${doc.siblingFiles.join(", ") || "none"}
|
|
92
|
-
|
|
93
|
-
# Current content
|
|
94
|
-
\`\`\`markdown
|
|
95
|
-
${doc.content}
|
|
96
|
-
\`\`\`
|
|
97
|
-
|
|
98
|
-
# Rules for the compacted file
|
|
99
|
-
- Write it in the SAME language as the current content. Never translate.
|
|
100
|
-
- Keep every invariant, constraint, security reason, and workflow step that the source files cannot show by
|
|
101
|
-
themselves. Merge duplicates instead of dropping either one.
|
|
102
|
-
- Delete what the code already states: field lists, types, labels, signatures, file lists, and imports.
|
|
103
|
-
- Delete scaffold and placeholder wording, changelog or migration history, TODOs, and any instruction about
|
|
104
|
-
reading or updating this file.
|
|
105
|
-
- Never invent a rule that the current content does not state.
|
|
106
|
-
- Keep every bullet to one short line, and stay under ${AbstractDoc.compactMinLines} lines in total.
|
|
107
|
-
- Shape:
|
|
108
|
-
${this.#targetShape(doc.kind)}
|
|
109
|
-
|
|
110
|
-
Respond with exactly one \`\`\`markdown code block holding the whole new file and nothing else.`;
|
|
111
|
-
}
|
|
112
|
-
}
|
package/aiEditor.test.ts
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import { parseTypescriptFileBlocks, preserveTypescriptResponseContent } from "./aiEditor";
|
|
3
|
-
|
|
4
|
-
describe("parseTypescriptFileBlocks", () => {
|
|
5
|
-
test("parses TypeScript file blocks with common fence variants", () => {
|
|
6
|
-
const writes = parseTypescriptFileBlocks(`
|
|
7
|
-
\`\`\`ts
|
|
8
|
-
|
|
9
|
-
// File: lib/car/car.constant.ts
|
|
10
|
-
|
|
11
|
-
export const car = "car";
|
|
12
|
-
\`\`\`
|
|
13
|
-
|
|
14
|
-
\`\`\`tsx
|
|
15
|
-
// File: lib/car/Car.Unit.tsx
|
|
16
|
-
export const CarUnit = () => null;
|
|
17
|
-
\`\`\`
|
|
18
|
-
`);
|
|
19
|
-
|
|
20
|
-
expect(writes).toEqual([
|
|
21
|
-
{
|
|
22
|
-
filePath: "lib/car/car.constant.ts",
|
|
23
|
-
content: 'export const car = "car";',
|
|
24
|
-
},
|
|
25
|
-
{
|
|
26
|
-
filePath: "lib/car/Car.Unit.tsx",
|
|
27
|
-
content: "export const CarUnit = () => null;",
|
|
28
|
-
},
|
|
29
|
-
]);
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
test("keeps previous code response when validation responds with prose only", () => {
|
|
33
|
-
const previousContent = `
|
|
34
|
-
\`\`\`typescript
|
|
35
|
-
// File: lib/car/car.constant.ts
|
|
36
|
-
export const car = "car";
|
|
37
|
-
\`\`\`
|
|
38
|
-
`;
|
|
39
|
-
const nextContent = "The generated file meets all specified requirements. No rewrite is necessary.";
|
|
40
|
-
|
|
41
|
-
expect(preserveTypescriptResponseContent(previousContent, nextContent)).toBe(previousContent);
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
test("uses next code response when validation rewrites with parseable files", () => {
|
|
45
|
-
const previousContent = `
|
|
46
|
-
\`\`\`typescript
|
|
47
|
-
// File: lib/car/car.constant.ts
|
|
48
|
-
export const car = "car";
|
|
49
|
-
\`\`\`
|
|
50
|
-
`;
|
|
51
|
-
const nextContent = `
|
|
52
|
-
\`\`\`typescript
|
|
53
|
-
// File: lib/car/car.constant.ts
|
|
54
|
-
export const car = "updated";
|
|
55
|
-
\`\`\`
|
|
56
|
-
`;
|
|
57
|
-
|
|
58
|
-
expect(preserveTypescriptResponseContent(previousContent, nextContent)).toBe(nextContent);
|
|
59
|
-
});
|
|
60
|
-
});
|