@payconductor/houndsman 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -39
- package/package.json +1 -1
- package/src/cli/commands/context-command.ts +11 -9
- package/src/cli/commands/doctor-command.ts +5 -0
- package/src/cli/commands/findings-command.ts +13 -3
- package/src/cli/commands/review-command.ts +1 -2
- package/src/cli/commands/status-command.ts +46 -0
- package/src/cli/help.ts +8 -3
- package/src/cli/index.ts +5 -2
- package/src/core/commands/context.ts +29 -13
- package/src/core/commands/findings.ts +216 -125
- package/src/core/commands/review.ts +7 -8
- package/src/core/commands/scaffold.ts +15 -16
- package/src/core/commands/status.ts +63 -0
- package/src/core/config.ts +16 -7
- package/src/core/engine/houndsman-engine.ts +15 -1
- package/src/core/engine/review-scope.ts +26 -8
- package/src/core/engine/review-session.ts +37 -9
- package/src/core/findings-state.ts +53 -75
- package/src/core/github-review.ts +620 -57
- package/src/core/jev/jev-candidates.ts +49 -35
- package/src/core/orchestrator.ts +271 -134
- package/src/core/paths.ts +26 -17
- package/src/core/pr-context.ts +19 -5
- package/src/core/project-config.ts +4 -5
- package/src/core/renderers/manifest-writer.ts +29 -18
- package/src/core/renderers/resume-renderer.ts +151 -0
- package/src/core/report-builder.ts +7 -4
- package/src/core/run-layout.ts +4 -104
- package/src/core/run-status.ts +59 -50
- package/src/core/services/git-repository.ts +16 -0
- package/src/core/services/project-files.ts +13 -1
- package/src/core/storage/factory.ts +24 -0
- package/src/core/storage/file/file-storage.ts +271 -0
- package/src/core/storage/paths.ts +64 -0
- package/src/core/storage/repo-identity.ts +70 -0
- package/src/core/storage/sqlite/mappers.ts +162 -0
- package/src/core/storage/sqlite/schema.ts +116 -0
- package/src/core/storage/sqlite/sqlite-storage.ts +431 -0
- package/src/core/storage/types.ts +152 -0
- package/src/core/storage/work-area.ts +191 -0
- package/src/index.ts +27 -6
- package/src/server/artifacts.ts +8 -61
- package/src/server/client/App.tsx +26 -9
- package/src/server/client/ArtifactsPanel.tsx +144 -23
- package/src/server/client/AutoRefresh.tsx +1 -1
- package/src/server/client/Filters.tsx +7 -2
- package/src/server/client/FindingCard.tsx +50 -15
- package/src/server/client/FindingsList.tsx +2 -2
- package/src/server/client/MarkdownText.tsx +181 -0
- package/src/server/client/ResumePanel.tsx +368 -0
- package/src/server/client/RunList.tsx +3 -3
- package/src/server/client/SubmitModal.tsx +58 -10
- package/src/server/client/SummaryBar.tsx +1 -1
- package/src/server/client/icons.tsx +17 -0
- package/src/server/client/utils/code-highlight.ts +422 -0
- package/src/server/client/utils/constants.ts +226 -0
- package/src/server/client/utils/json-highlight.ts +55 -0
- package/src/server/client/utils/markdown.ts +319 -0
- package/src/server/client/{types.ts → utils/types.ts} +81 -0
- package/src/server/index.ts +154 -16
- package/src/server/run-repository.ts +153 -136
- package/src/skill/OUTPUT-FORMAT.md +6 -4
- package/src/skill/README.md +22 -14
- package/src/skill/SETUP.md +4 -4
- package/src/skill/example-review.md +22 -14
- package/src/skill/fix.md +5 -5
- package/src/skill/hunt.md +55 -32
- package/src/skill/train.md +1 -1
- package/src/skill/verify.md +7 -6
- package/src/tests/__snapshots__/baseline.test.ts.snap +74 -61
- package/src/tests/baseline.test.ts +79 -34
- package/src/tests/cli.test.ts +40 -19
- package/src/tests/code-highlight.test.ts +123 -0
- package/src/tests/domain.test.ts +1 -1
- package/src/tests/findings-commands.test.ts +153 -131
- package/src/tests/findings-state.test.ts +56 -5
- package/src/tests/fixtures/fake-project.ts +0 -1
- package/src/tests/github-review.test.ts +409 -60
- package/src/tests/infrastructure.test.ts +33 -38
- package/src/tests/markdown.test.ts +149 -0
- package/src/tests/review-resume.test.ts +101 -0
- package/src/tests/review-scope.test.ts +302 -0
- package/src/tests/review-scripts.test.ts +81 -5
- package/src/tests/run-status-view.test.ts +94 -0
- package/src/tests/run-status.test.ts +86 -23
- package/src/tests/server.test.ts +184 -96
- package/src/tests/storage.test.ts +449 -0
- package/src/server/client/constants.ts +0 -91
- /package/src/server/client/{api.ts → utils/api.ts} +0 -0
- /package/src/server/client/{format.ts → utils/format.ts} +0 -0
|
@@ -26,13 +26,16 @@ import {
|
|
|
26
26
|
writeUserLayer,
|
|
27
27
|
} from "../core/commands/scaffold";
|
|
28
28
|
import { defineHoundsmanConfig } from "../core/config";
|
|
29
|
-
import { defaultOptions, finalize, prepare } from "../core/orchestrator";
|
|
29
|
+
import { defaultOptions, finalize, prepare, reviewContext } from "../core/orchestrator";
|
|
30
|
+
import { displayPath } from "../core/paths";
|
|
30
31
|
import {
|
|
31
32
|
type MergedFinding,
|
|
32
33
|
renderDomainReport,
|
|
33
34
|
renderReport,
|
|
34
35
|
type WatchdogReport,
|
|
35
36
|
} from "../core/report-builder";
|
|
37
|
+
import { resolveRepoIdentity } from "../core/storage/repo-identity";
|
|
38
|
+
import { SQLiteStorageProvider } from "../core/storage/sqlite/sqlite-storage";
|
|
36
39
|
import { defineWatchdog } from "../core/types";
|
|
37
40
|
import * as facade from "../index";
|
|
38
41
|
|
|
@@ -56,7 +59,6 @@ const baselineWatchdog = defineWatchdog({
|
|
|
56
59
|
|
|
57
60
|
const baselineConfig = defineHoundsmanConfig({
|
|
58
61
|
projectName: "Baseline",
|
|
59
|
-
artifactDir: ".houndsman/.runs",
|
|
60
62
|
watchdogs: { alpha: baselineWatchdog },
|
|
61
63
|
exclusions: [/^dist\//],
|
|
62
64
|
docMappings: [[/^src\//, ["README.md"]]],
|
|
@@ -75,6 +77,10 @@ function normalizeArtifacts(text: string): string {
|
|
|
75
77
|
'"$1": "<timestamp>"',
|
|
76
78
|
)
|
|
77
79
|
.replace(/"\d{4}-\d{2}-\d{2}T[^"]*"/g, '"<timestamp>"')
|
|
80
|
+
.replace(
|
|
81
|
+
/\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/g,
|
|
82
|
+
"<uuid>",
|
|
83
|
+
)
|
|
78
84
|
.replace(/\b[0-9a-f]{40}\b/g, "<sha>")
|
|
79
85
|
.replace(/\b[0-9a-f]{16}\b/g, "<fingerprint>")
|
|
80
86
|
.replace(/\b[0-9a-f]{10}\b/g, "<id>")
|
|
@@ -128,7 +134,7 @@ describe("baseline — brief", () => {
|
|
|
128
134
|
overflow: ["src/alpha/c.ts"],
|
|
129
135
|
deleted: ["src/alpha/old.ts"],
|
|
130
136
|
branch: "feature/x",
|
|
131
|
-
outputPath: "
|
|
137
|
+
outputPath: "/workspace/work/review-x/findings/alpha.json",
|
|
132
138
|
outputFormatPath: ".houndsman/core/skill/OUTPUT-FORMAT.md",
|
|
133
139
|
docs: ["AGENTS.md", "README.md"],
|
|
134
140
|
context: {
|
|
@@ -317,6 +323,8 @@ describe("baseline — artefatos de prepare/finalize", () => {
|
|
|
317
323
|
test("gera classification, brief, manifest, review e state", async () => {
|
|
318
324
|
const cwd = mkdtempSync(join(tmpdir(), "houndsman-baseline-run-"));
|
|
319
325
|
const originalCwd = process.cwd();
|
|
326
|
+
const originalHome = process.env.HOUNDSMAN_HOME;
|
|
327
|
+
let storage: SQLiteStorageProvider | null = null;
|
|
320
328
|
|
|
321
329
|
try {
|
|
322
330
|
git(cwd, ["init", "-b", "main"]);
|
|
@@ -343,30 +351,58 @@ describe("baseline — artefatos de prepare/finalize", () => {
|
|
|
343
351
|
git(cwd, ["commit", "-m", "change"]);
|
|
344
352
|
|
|
345
353
|
const opts = {
|
|
346
|
-
...defaultOptions(
|
|
354
|
+
...defaultOptions(),
|
|
347
355
|
base,
|
|
348
356
|
head: "HEAD",
|
|
349
357
|
noJev: true,
|
|
350
358
|
};
|
|
351
359
|
|
|
360
|
+
process.env.HOUNDSMAN_HOME = join(cwd, ".home");
|
|
352
361
|
process.chdir(cwd);
|
|
353
362
|
|
|
354
|
-
const
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
363
|
+
const config = {
|
|
364
|
+
...baselineConfig,
|
|
365
|
+
storage: {
|
|
366
|
+
provider: "sqlite" as const,
|
|
367
|
+
path: join(cwd, "houndsman.db"),
|
|
368
|
+
},
|
|
369
|
+
};
|
|
370
|
+
storage = new SQLiteStorageProvider(join(cwd, "houndsman.db"));
|
|
371
|
+
const repo = resolveRepoIdentity();
|
|
372
|
+
const review = storage.getOrCreateReview({
|
|
373
|
+
repo,
|
|
374
|
+
branch: "main",
|
|
375
|
+
slug: "main",
|
|
376
|
+
});
|
|
377
|
+
const ctx = reviewContext(storage, review);
|
|
378
|
+
const homeDisplay = displayPath(cwd);
|
|
379
|
+
const escapedCwd = cwd.replace(/\\/g, "\\\\");
|
|
380
|
+
const normalize = (text: string): string =>
|
|
381
|
+
normalizeArtifacts(
|
|
382
|
+
text
|
|
383
|
+
.replaceAll(escapedCwd, "<cwd>")
|
|
384
|
+
.replaceAll(cwd, "<cwd>")
|
|
385
|
+
.replaceAll(cwd.replace(/\\/g, "/"), "<cwd>")
|
|
386
|
+
.replaceAll(cwd.replace(/\\/g, "/").toLowerCase(), "<cwd>")
|
|
387
|
+
.replaceAll(homeDisplay, "<cwd>"),
|
|
388
|
+
);
|
|
389
|
+
|
|
390
|
+
const prepared = await prepare(opts, "main", config, ctx);
|
|
391
|
+
expect(normalize(prepared.messages.join("\n"))).toMatchSnapshot();
|
|
392
|
+
|
|
393
|
+
for (const name of ["classification.json", "manifest.json"]) {
|
|
394
|
+
const artifact = storage.readArtifact(review.id, name);
|
|
395
|
+
expect(normalize(artifact?.content ?? "")).toMatchSnapshot(name);
|
|
366
396
|
}
|
|
397
|
+
const workBrief = readFileSync(ctx.work.briefFile("alpha"), "utf8");
|
|
398
|
+
const briefArtifact = storage.readArtifact(review.id, "briefs/alpha.md");
|
|
399
|
+
expect(briefArtifact?.content).toBe(workBrief);
|
|
400
|
+
expect(normalize(briefArtifact?.content ?? "")).toMatchSnapshot(
|
|
401
|
+
"briefs/alpha.md",
|
|
402
|
+
);
|
|
367
403
|
|
|
368
404
|
writeFileSync(
|
|
369
|
-
|
|
405
|
+
ctx.work.findingFile("alpha"),
|
|
370
406
|
`${JSON.stringify(
|
|
371
407
|
{
|
|
372
408
|
watchdogId: "alpha",
|
|
@@ -389,21 +425,27 @@ describe("baseline — artefatos de prepare/finalize", () => {
|
|
|
389
425
|
)}\n`,
|
|
390
426
|
);
|
|
391
427
|
|
|
392
|
-
const finalized = finalize(opts, "main",
|
|
393
|
-
expect(
|
|
394
|
-
|
|
395
|
-
for (const
|
|
396
|
-
|
|
397
|
-
"
|
|
398
|
-
"state.json",
|
|
399
|
-
"run.json",
|
|
400
|
-
]) {
|
|
401
|
-
expect(
|
|
402
|
-
normalizeArtifacts(readFileSync(join(runDir, file), "utf8")),
|
|
403
|
-
).toMatchSnapshot(file);
|
|
428
|
+
const finalized = finalize(opts, "main", config, ctx);
|
|
429
|
+
expect(normalize(finalized.messages.join("\n"))).toMatchSnapshot();
|
|
430
|
+
|
|
431
|
+
for (const name of ["review.md", "review.alpha.md"]) {
|
|
432
|
+
const artifact = storage.readArtifact(review.id, name);
|
|
433
|
+
expect(normalize(artifact?.content ?? "")).toMatchSnapshot(name);
|
|
404
434
|
}
|
|
435
|
+
const resume = storage.readArtifact(review.id, "review-resume.md");
|
|
436
|
+
expect(resume?.kind).toBe("resume");
|
|
437
|
+
expect((resume?.content ?? "").length).toBeLessThanOrEqual(2500);
|
|
438
|
+
expect(
|
|
439
|
+
normalize(JSON.stringify(storage.readFindings(review.id), null, 2)),
|
|
440
|
+
).toMatchSnapshot("state.json");
|
|
441
|
+
expect(
|
|
442
|
+
normalize(JSON.stringify(storage.getReview(review.id), null, 2)),
|
|
443
|
+
).toMatchSnapshot("run.json");
|
|
405
444
|
} finally {
|
|
445
|
+
storage?.close();
|
|
406
446
|
process.chdir(originalCwd);
|
|
447
|
+
if (originalHome === undefined) delete process.env.HOUNDSMAN_HOME;
|
|
448
|
+
else process.env.HOUNDSMAN_HOME = originalHome;
|
|
407
449
|
rmSync(cwd, { recursive: true, force: true });
|
|
408
450
|
}
|
|
409
451
|
}, 30_000);
|
|
@@ -433,12 +475,19 @@ const REQUIRED_EXPORTS = [
|
|
|
433
475
|
"GitRepository",
|
|
434
476
|
"ProjectConfig",
|
|
435
477
|
"ProjectFiles",
|
|
436
|
-
"RunLayout",
|
|
437
478
|
"Watchdog",
|
|
438
479
|
"WatchdogRegistry",
|
|
439
480
|
"systemClock",
|
|
440
481
|
"HoundsmanServer",
|
|
441
482
|
"HoundsmanEngine",
|
|
483
|
+
"ReviewWorkArea",
|
|
484
|
+
"SQLiteStorageProvider",
|
|
485
|
+
"FileStorageProvider",
|
|
486
|
+
"createStorage",
|
|
487
|
+
"createMemoryStorage",
|
|
488
|
+
"resolveRepoIdentity",
|
|
489
|
+
"buildRepoIdentity",
|
|
490
|
+
"normalizeRemote",
|
|
442
491
|
"ReviewSession",
|
|
443
492
|
"ReviewScopeResolver",
|
|
444
493
|
"buildCarryover",
|
|
@@ -447,7 +496,6 @@ const REQUIRED_EXPORTS = [
|
|
|
447
496
|
"ArgParser",
|
|
448
497
|
"ProjectScaffolder",
|
|
449
498
|
"FixBatchBuilder",
|
|
450
|
-
"Verifier",
|
|
451
499
|
"BriefRenderer",
|
|
452
500
|
"ReportRenderer",
|
|
453
501
|
"ManifestWriter",
|
|
@@ -455,7 +503,6 @@ const REQUIRED_EXPORTS = [
|
|
|
455
503
|
"assetPath",
|
|
456
504
|
"buildAnchor",
|
|
457
505
|
"buildStateFromFindings",
|
|
458
|
-
"buildStateFromRunDir",
|
|
459
506
|
"checkRegistry",
|
|
460
507
|
"churnOf",
|
|
461
508
|
"classifyEntries",
|
|
@@ -492,14 +539,12 @@ const REQUIRED_EXPORTS = [
|
|
|
492
539
|
"resolveConfigPath",
|
|
493
540
|
"resolveJevConcurrency",
|
|
494
541
|
"resolveMaxFilesPerBrief",
|
|
495
|
-
"runDir",
|
|
496
542
|
"runFindingsCommand",
|
|
497
543
|
"runVerifyCommand",
|
|
498
544
|
"selectByBudget",
|
|
499
545
|
"severityRank",
|
|
500
546
|
"sortByRisk",
|
|
501
547
|
"startServer",
|
|
502
|
-
"stateFile",
|
|
503
548
|
"summarize",
|
|
504
549
|
"syncState",
|
|
505
550
|
] as const;
|
package/src/tests/cli.test.ts
CHANGED
|
@@ -7,9 +7,13 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { describe, expect, test } from "bun:test";
|
|
10
|
-
import {
|
|
10
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
11
11
|
import { tmpdir } from "node:os";
|
|
12
12
|
import { join } from "node:path";
|
|
13
|
+
import { buildStateFromFindings, fileReader } from "../core/findings-state";
|
|
14
|
+
import { dedupeFindings, parseWatchdogReport } from "../core/report-builder";
|
|
15
|
+
import { buildRepoIdentity } from "../core/storage/repo-identity";
|
|
16
|
+
import { SQLiteStorageProvider } from "../core/storage/sqlite/sqlite-storage";
|
|
13
17
|
|
|
14
18
|
/** Caminho do executável do CLI (TS, rodado pelo Bun). */
|
|
15
19
|
const CLI = join(import.meta.dir, "..", "cli", "index.ts");
|
|
@@ -17,6 +21,7 @@ const CLI = join(import.meta.dir, "..", "cli", "index.ts");
|
|
|
17
21
|
/** Config mínima, como objeto literal (o `loadConfig` deriva `watchdogIds`). */
|
|
18
22
|
const CONFIG = `export default {
|
|
19
23
|
projectName: "Smoke",
|
|
24
|
+
storage: { provider: "sqlite", path: "./houndsman.db" },
|
|
20
25
|
watchdogs: {
|
|
21
26
|
alpha: {
|
|
22
27
|
id: "alpha",
|
|
@@ -33,22 +38,18 @@ const CONFIG = `export default {
|
|
|
33
38
|
};
|
|
34
39
|
`;
|
|
35
40
|
|
|
36
|
-
type Project = { cwd: string;
|
|
41
|
+
type Project = { cwd: string; cleanup: () => void };
|
|
37
42
|
|
|
38
|
-
/** Projeto temporário: config + uma run
|
|
43
|
+
/** Projeto temporário: config + banco com uma run e um finding. */
|
|
39
44
|
function makeProject(): Project {
|
|
40
45
|
const cwd = mkdtempSync(join(tmpdir(), "houndsman-cli-"));
|
|
41
|
-
const artifacts = join(cwd, ".houndsman", ".runs");
|
|
42
|
-
const runDir = join(artifacts, "main");
|
|
43
|
-
mkdirSync(join(runDir, "findings"), { recursive: true });
|
|
44
46
|
writeFileSync(join(cwd, "houndsman.config.ts"), CONFIG);
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
);
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
JSON.stringify({
|
|
47
|
+
|
|
48
|
+
const storage = new SQLiteStorageProvider(join(cwd, "houndsman.db"));
|
|
49
|
+
const repo = buildRepoIdentity(null, cwd);
|
|
50
|
+
const review = storage.getOrCreateReview({ repo, branch: "main", slug: "main" });
|
|
51
|
+
const report = parseWatchdogReport(
|
|
52
|
+
{
|
|
52
53
|
watchdogId: "alpha",
|
|
53
54
|
reviewedFiles: ["src/a.ts"],
|
|
54
55
|
skipped: [],
|
|
@@ -62,11 +63,22 @@ function makeProject(): Project {
|
|
|
62
63
|
action: "tipar",
|
|
63
64
|
},
|
|
64
65
|
],
|
|
65
|
-
}
|
|
66
|
+
},
|
|
67
|
+
"alpha.json",
|
|
66
68
|
);
|
|
69
|
+
if (report) {
|
|
70
|
+
storage.writeFindings(
|
|
71
|
+
review.id,
|
|
72
|
+
buildStateFromFindings(dedupeFindings([report]), {
|
|
73
|
+
branch: "main",
|
|
74
|
+
readFile: fileReader(cwd),
|
|
75
|
+
}),
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
storage.close();
|
|
79
|
+
|
|
67
80
|
return {
|
|
68
81
|
cwd,
|
|
69
|
-
artifacts,
|
|
70
82
|
cleanup: () => rmSync(cwd, { recursive: true, force: true }),
|
|
71
83
|
};
|
|
72
84
|
}
|
|
@@ -112,8 +124,6 @@ describe("cli smoke — imports dinâmicos", () => {
|
|
|
112
124
|
"findings",
|
|
113
125
|
"summary",
|
|
114
126
|
"--json",
|
|
115
|
-
"--dir",
|
|
116
|
-
project.artifacts,
|
|
117
127
|
"--branch",
|
|
118
128
|
"main",
|
|
119
129
|
]);
|
|
@@ -132,8 +142,6 @@ describe("cli smoke — imports dinâmicos", () => {
|
|
|
132
142
|
"submit",
|
|
133
143
|
"--dry-run",
|
|
134
144
|
"--json",
|
|
135
|
-
"--dir",
|
|
136
|
-
project.artifacts,
|
|
137
145
|
"--branch",
|
|
138
146
|
"main",
|
|
139
147
|
]);
|
|
@@ -160,6 +168,19 @@ describe("cli smoke — imports dinâmicos", () => {
|
|
|
160
168
|
try {
|
|
161
169
|
const result = runCli(project.cwd, ["context", "--branch", "main"]);
|
|
162
170
|
expect(result.code).toBe(0);
|
|
171
|
+
|
|
172
|
+
// Auditoria: o brief do contexto fica persistido mesmo sem PR.
|
|
173
|
+
const storage = new SQLiteStorageProvider(join(project.cwd, "houndsman.db"));
|
|
174
|
+
try {
|
|
175
|
+
const repo = buildRepoIdentity(null, project.cwd);
|
|
176
|
+
const review = storage.findReview(repo.key, "main");
|
|
177
|
+
expect(review).not.toBeNull();
|
|
178
|
+
expect(
|
|
179
|
+
storage.readArtifact(review?.id ?? "", "context-brief.md")?.kind,
|
|
180
|
+
).toBe("context-brief");
|
|
181
|
+
} finally {
|
|
182
|
+
storage.close();
|
|
183
|
+
}
|
|
163
184
|
} finally {
|
|
164
185
|
project.cleanup();
|
|
165
186
|
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Destaque de código do evidence — tokenizer próprio, sem dependência.
|
|
3
|
+
*
|
|
4
|
+
* Garante que a inferência por extensão funciona, que o texto original é
|
|
5
|
+
* preservado e que os tokens principais são classificados.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, expect, test } from "bun:test";
|
|
9
|
+
import {
|
|
10
|
+
highlightCode,
|
|
11
|
+
languageFromFile,
|
|
12
|
+
languageFromName,
|
|
13
|
+
} from "../server/client/utils/code-highlight";
|
|
14
|
+
|
|
15
|
+
/** O texto concatenado dos tokens deve ser idêntico ao input. */
|
|
16
|
+
function joined(tokens: { text: string }[]): string {
|
|
17
|
+
return tokens.map((token) => token.text).join("");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe("languageFromFile", () => {
|
|
21
|
+
test("infere pela extensão e ignora extensões desconhecidas", () => {
|
|
22
|
+
expect(languageFromFile("src/a.ts")).toBe("typescript");
|
|
23
|
+
expect(languageFromFile("src/a.tsx")).toBe("typescript");
|
|
24
|
+
expect(languageFromFile("scripts/a.py")).toBe("python");
|
|
25
|
+
expect(languageFromFile("db/a.sql")).toBe("sql");
|
|
26
|
+
expect(languageFromFile("charts/a.yaml")).toBe("yaml");
|
|
27
|
+
expect(languageFromFile("README.md")).toBeNull();
|
|
28
|
+
expect(languageFromFile("Makefile")).toBeNull();
|
|
29
|
+
expect(languageFromFile("a.unknown")).toBeNull();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("aliases de fence markdown", () => {
|
|
33
|
+
expect(languageFromName("ts")).toBe("typescript");
|
|
34
|
+
expect(languageFromName("js")).toBe("javascript");
|
|
35
|
+
expect(languageFromName("py")).toBe("python");
|
|
36
|
+
expect(languageFromName("json")).toBe("json");
|
|
37
|
+
expect(languageFromName("yml")).toBe("yaml");
|
|
38
|
+
expect(languageFromName("cobol")).toBeNull();
|
|
39
|
+
expect(languageFromName(null)).toBeNull();
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe("highlightCode", () => {
|
|
44
|
+
test("TypeScript: keywords, tipos, números, strings e comentários", () => {
|
|
45
|
+
const input = 'const answer: number = 42; // resposta\nconst url = "https://x";';
|
|
46
|
+
const result = highlightCode(input, "typescript");
|
|
47
|
+
expect(result?.label).toBe("TypeScript");
|
|
48
|
+
expect(joined(result?.tokens ?? [])).toBe(input);
|
|
49
|
+
expect(result?.tokens.find((token) => token.text === "const")?.type).toBe(
|
|
50
|
+
"keyword",
|
|
51
|
+
);
|
|
52
|
+
expect(result?.tokens.find((token) => token.text === "number")?.type).toBe(
|
|
53
|
+
"type",
|
|
54
|
+
);
|
|
55
|
+
expect(result?.tokens.find((token) => token.text === "42")?.type).toBe("number");
|
|
56
|
+
expect(result?.tokens.find((token) => token.text === "// resposta")?.type).toBe(
|
|
57
|
+
"comment",
|
|
58
|
+
);
|
|
59
|
+
expect(result?.tokens.find((token) => token.text === '"https://x"')?.type).toBe(
|
|
60
|
+
"string",
|
|
61
|
+
);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("Python: comentários com # e strings de três aspas", () => {
|
|
65
|
+
const input = 'def greet(name):\n """doc"""\n return "hi" # ok';
|
|
66
|
+
const result = highlightCode(input, "python");
|
|
67
|
+
expect(joined(result?.tokens ?? [])).toBe(input);
|
|
68
|
+
expect(result?.tokens.find((token) => token.text === "def")?.type).toBe(
|
|
69
|
+
"keyword",
|
|
70
|
+
);
|
|
71
|
+
expect(result?.tokens.find((token) => token.text === "return")?.type).toBe(
|
|
72
|
+
"keyword",
|
|
73
|
+
);
|
|
74
|
+
expect(result?.tokens.find((token) => token.text === '"""doc"""')?.type).toBe(
|
|
75
|
+
"string",
|
|
76
|
+
);
|
|
77
|
+
expect(result?.tokens.find((token) => token.text === "# ok")?.type).toBe(
|
|
78
|
+
"comment",
|
|
79
|
+
);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("JSON: chave vira propriedade e literal vira keyword", () => {
|
|
83
|
+
const input = '{"a": 1, "b": true}';
|
|
84
|
+
const result = highlightCode(input, "json");
|
|
85
|
+
expect(result?.label).toBe("JSON");
|
|
86
|
+
expect(joined(result?.tokens ?? [])).toBe(input);
|
|
87
|
+
expect(result?.tokens.find((token) => token.text === '"a"')?.type).toBe(
|
|
88
|
+
"property",
|
|
89
|
+
);
|
|
90
|
+
expect(result?.tokens.find((token) => token.text === "true")?.type).toBe(
|
|
91
|
+
"keyword",
|
|
92
|
+
);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("HTML: tag e atributo", () => {
|
|
96
|
+
const input = '<div class="x">hi</div>';
|
|
97
|
+
const result = highlightCode(input, "html");
|
|
98
|
+
expect(joined(result?.tokens ?? [])).toBe(input);
|
|
99
|
+
expect(result?.tokens.find((token) => token.text === "div")?.type).toBe(
|
|
100
|
+
"keyword",
|
|
101
|
+
);
|
|
102
|
+
expect(result?.tokens.find((token) => token.text === "class")?.type).toBe(
|
|
103
|
+
"property",
|
|
104
|
+
);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("YAML: chave no início da linha e comentário", () => {
|
|
108
|
+
const input = "name: value # note";
|
|
109
|
+
const result = highlightCode(input, "yaml");
|
|
110
|
+
expect(joined(result?.tokens ?? [])).toBe(input);
|
|
111
|
+
expect(result?.tokens.find((token) => token.text === "name")?.type).toBe(
|
|
112
|
+
"property",
|
|
113
|
+
);
|
|
114
|
+
expect(result?.tokens.find((token) => token.text === "# note")?.type).toBe(
|
|
115
|
+
"comment",
|
|
116
|
+
);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("sem linguagem/desconhecida devolve null", () => {
|
|
120
|
+
expect(highlightCode("const x = 1", null)).toBeNull();
|
|
121
|
+
expect(highlightCode("const x = 1", "cobol")).toBeNull();
|
|
122
|
+
});
|
|
123
|
+
});
|
package/src/tests/domain.test.ts
CHANGED
|
@@ -102,7 +102,7 @@ describe("ProjectConfig", () => {
|
|
|
102
102
|
test("resolve defaults e valores do projeto", () => {
|
|
103
103
|
const project = new ProjectConfig(fakeHoundsmanConfig);
|
|
104
104
|
expect(project.projectName).toBe("Fake Project");
|
|
105
|
-
expect(project.
|
|
105
|
+
expect(project.storage).toEqual({});
|
|
106
106
|
expect(project.maxFilesPerBrief).toBe(40);
|
|
107
107
|
expect(project.jevConcurrency).toBe(5);
|
|
108
108
|
expect(project.alwaysIncludeDocs).toEqual(["README.md"]);
|