@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.
Files changed (91) hide show
  1. package/README.md +25 -39
  2. package/package.json +1 -1
  3. package/src/cli/commands/context-command.ts +11 -9
  4. package/src/cli/commands/doctor-command.ts +5 -0
  5. package/src/cli/commands/findings-command.ts +13 -3
  6. package/src/cli/commands/review-command.ts +1 -2
  7. package/src/cli/commands/status-command.ts +46 -0
  8. package/src/cli/help.ts +8 -3
  9. package/src/cli/index.ts +5 -2
  10. package/src/core/commands/context.ts +29 -13
  11. package/src/core/commands/findings.ts +216 -125
  12. package/src/core/commands/review.ts +7 -8
  13. package/src/core/commands/scaffold.ts +15 -16
  14. package/src/core/commands/status.ts +63 -0
  15. package/src/core/config.ts +16 -7
  16. package/src/core/engine/houndsman-engine.ts +15 -1
  17. package/src/core/engine/review-scope.ts +26 -8
  18. package/src/core/engine/review-session.ts +37 -9
  19. package/src/core/findings-state.ts +53 -75
  20. package/src/core/github-review.ts +620 -57
  21. package/src/core/jev/jev-candidates.ts +49 -35
  22. package/src/core/orchestrator.ts +271 -134
  23. package/src/core/paths.ts +26 -17
  24. package/src/core/pr-context.ts +19 -5
  25. package/src/core/project-config.ts +4 -5
  26. package/src/core/renderers/manifest-writer.ts +29 -18
  27. package/src/core/renderers/resume-renderer.ts +151 -0
  28. package/src/core/report-builder.ts +7 -4
  29. package/src/core/run-layout.ts +4 -104
  30. package/src/core/run-status.ts +59 -50
  31. package/src/core/services/git-repository.ts +16 -0
  32. package/src/core/services/project-files.ts +13 -1
  33. package/src/core/storage/factory.ts +24 -0
  34. package/src/core/storage/file/file-storage.ts +271 -0
  35. package/src/core/storage/paths.ts +64 -0
  36. package/src/core/storage/repo-identity.ts +70 -0
  37. package/src/core/storage/sqlite/mappers.ts +162 -0
  38. package/src/core/storage/sqlite/schema.ts +116 -0
  39. package/src/core/storage/sqlite/sqlite-storage.ts +431 -0
  40. package/src/core/storage/types.ts +152 -0
  41. package/src/core/storage/work-area.ts +191 -0
  42. package/src/index.ts +27 -6
  43. package/src/server/artifacts.ts +8 -61
  44. package/src/server/client/App.tsx +26 -9
  45. package/src/server/client/ArtifactsPanel.tsx +144 -23
  46. package/src/server/client/AutoRefresh.tsx +1 -1
  47. package/src/server/client/Filters.tsx +7 -2
  48. package/src/server/client/FindingCard.tsx +50 -15
  49. package/src/server/client/FindingsList.tsx +2 -2
  50. package/src/server/client/MarkdownText.tsx +181 -0
  51. package/src/server/client/ResumePanel.tsx +368 -0
  52. package/src/server/client/RunList.tsx +3 -3
  53. package/src/server/client/SubmitModal.tsx +58 -10
  54. package/src/server/client/SummaryBar.tsx +1 -1
  55. package/src/server/client/icons.tsx +17 -0
  56. package/src/server/client/utils/code-highlight.ts +422 -0
  57. package/src/server/client/utils/constants.ts +226 -0
  58. package/src/server/client/utils/json-highlight.ts +55 -0
  59. package/src/server/client/utils/markdown.ts +319 -0
  60. package/src/server/client/{types.ts → utils/types.ts} +81 -0
  61. package/src/server/index.ts +154 -16
  62. package/src/server/run-repository.ts +153 -136
  63. package/src/skill/OUTPUT-FORMAT.md +6 -4
  64. package/src/skill/README.md +22 -14
  65. package/src/skill/SETUP.md +4 -4
  66. package/src/skill/example-review.md +22 -14
  67. package/src/skill/fix.md +5 -5
  68. package/src/skill/hunt.md +55 -32
  69. package/src/skill/train.md +1 -1
  70. package/src/skill/verify.md +7 -6
  71. package/src/tests/__snapshots__/baseline.test.ts.snap +74 -61
  72. package/src/tests/baseline.test.ts +79 -34
  73. package/src/tests/cli.test.ts +40 -19
  74. package/src/tests/code-highlight.test.ts +123 -0
  75. package/src/tests/domain.test.ts +1 -1
  76. package/src/tests/findings-commands.test.ts +153 -131
  77. package/src/tests/findings-state.test.ts +56 -5
  78. package/src/tests/fixtures/fake-project.ts +0 -1
  79. package/src/tests/github-review.test.ts +409 -60
  80. package/src/tests/infrastructure.test.ts +33 -38
  81. package/src/tests/markdown.test.ts +149 -0
  82. package/src/tests/review-resume.test.ts +101 -0
  83. package/src/tests/review-scope.test.ts +302 -0
  84. package/src/tests/review-scripts.test.ts +81 -5
  85. package/src/tests/run-status-view.test.ts +94 -0
  86. package/src/tests/run-status.test.ts +86 -23
  87. package/src/tests/server.test.ts +184 -96
  88. package/src/tests/storage.test.ts +449 -0
  89. package/src/server/client/constants.ts +0 -91
  90. /package/src/server/client/{api.ts → utils/api.ts} +0 -0
  91. /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: ".houndsman/.runs/feature-x/findings/alpha.json",
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(".houndsman/.runs"),
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 prepared = await prepare(opts, "main", baselineConfig);
355
- expect(normalizeArtifacts(prepared.messages.join("\n"))).toMatchSnapshot();
356
-
357
- const runDir = join(cwd, ".houndsman", ".runs", "main");
358
- for (const file of [
359
- "classification.json",
360
- "manifest.json",
361
- "briefs/alpha.md",
362
- ]) {
363
- expect(
364
- normalizeArtifacts(readFileSync(join(runDir, file), "utf8")),
365
- ).toMatchSnapshot(file);
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
- join(runDir, "findings", "alpha.json"),
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", baselineConfig);
393
- expect(normalizeArtifacts(finalized.messages.join("\n"))).toMatchSnapshot();
394
-
395
- for (const file of [
396
- "review.md",
397
- "review.alpha.md",
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;
@@ -7,9 +7,13 @@
7
7
  */
8
8
 
9
9
  import { describe, expect, test } from "bun:test";
10
- import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
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; artifacts: string; cleanup: () => void };
41
+ type Project = { cwd: string; cleanup: () => void };
37
42
 
38
- /** Projeto temporário: config + uma run com um finding. */
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
- writeFileSync(
46
- join(runDir, "classification.json"),
47
- JSON.stringify({ watchdogs: { alpha: ["src/a.ts"] } }),
48
- );
49
- writeFileSync(
50
- join(runDir, "findings", "alpha.json"),
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
+ });
@@ -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.artifactDir).toBe("tmp/fake-review");
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"]);