github-issue-tower-defence-management 1.152.3 → 1.153.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/CHANGELOG.md +7 -0
- package/README.md +16 -1
- package/bin/adapter/entry-points/cli/index.js +36 -0
- package/bin/adapter/entry-points/cli/index.js.map +1 -1
- package/bin/adapter/entry-points/console/webServer.js +59 -23
- package/bin/adapter/entry-points/console/webServer.js.map +1 -1
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +11 -0
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
- package/bin/adapter/entry-points/handlers/ownerCallFileCleaner.js +20 -0
- package/bin/adapter/entry-points/handlers/ownerCallFileCleaner.js.map +1 -0
- package/bin/adapter/entry-points/handlers/ownerCallFileStore.js +80 -0
- package/bin/adapter/entry-points/handlers/ownerCallFileStore.js.map +1 -0
- package/bin/domain/usecases/intmux/OwnerCallFile.js +52 -0
- package/bin/domain/usecases/intmux/OwnerCallFile.js.map +1 -0
- package/package.json +1 -1
- package/src/adapter/entry-points/cli/index.test.ts +189 -0
- package/src/adapter/entry-points/cli/index.ts +75 -0
- package/src/adapter/entry-points/console/webServer.test.ts +317 -0
- package/src/adapter/entry-points/console/webServer.ts +86 -26
- package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.test.ts +35 -0
- package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +15 -0
- package/src/adapter/entry-points/handlers/ownerCallFileCleaner.test.ts +109 -0
- package/src/adapter/entry-points/handlers/ownerCallFileCleaner.ts +25 -0
- package/src/adapter/entry-points/handlers/ownerCallFileStore.test.ts +286 -0
- package/src/adapter/entry-points/handlers/ownerCallFileStore.ts +145 -0
- package/src/domain/usecases/intmux/OwnerCallFile.test.ts +184 -0
- package/src/domain/usecases/intmux/OwnerCallFile.ts +80 -0
- package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
- package/types/adapter/entry-points/console/webServer.d.ts +1 -0
- package/types/adapter/entry-points/console/webServer.d.ts.map +1 -1
- package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
- package/types/adapter/entry-points/handlers/ownerCallFileCleaner.d.ts +8 -0
- package/types/adapter/entry-points/handlers/ownerCallFileCleaner.d.ts.map +1 -0
- package/types/adapter/entry-points/handlers/ownerCallFileStore.d.ts +21 -0
- package/types/adapter/entry-points/handlers/ownerCallFileStore.d.ts.map +1 -0
- package/types/domain/usecases/intmux/OwnerCallFile.d.ts +18 -0
- package/types/domain/usecases/intmux/OwnerCallFile.d.ts.map +1 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.cleanClosedIssueOwnerCallFiles = void 0;
|
|
4
|
+
const InTmuxByHumanSessionReconcileUseCase_1 = require("../../../domain/usecases/intmux/InTmuxByHumanSessionReconcileUseCase");
|
|
5
|
+
const ownerCallFileStore_1 = require("./ownerCallFileStore");
|
|
6
|
+
const cleanClosedIssueOwnerCallFiles = (params) => {
|
|
7
|
+
const { inTmuxDataOutputDir, pjcode, issues } = params;
|
|
8
|
+
if (!inTmuxDataOutputDir || !pjcode) {
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
for (const issue of issues.filter((candidate) => candidate.isClosed)) {
|
|
12
|
+
(0, ownerCallFileStore_1.ownerCallFileDelete)({
|
|
13
|
+
dataDir: inTmuxDataOutputDir,
|
|
14
|
+
projectCode: pjcode,
|
|
15
|
+
sessionName: (0, InTmuxByHumanSessionReconcileUseCase_1.toTmuxSessionName)(issue.url),
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
exports.cleanClosedIssueOwnerCallFiles = cleanClosedIssueOwnerCallFiles;
|
|
20
|
+
//# sourceMappingURL=ownerCallFileCleaner.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ownerCallFileCleaner.js","sourceRoot":"","sources":["../../../../src/adapter/entry-points/handlers/ownerCallFileCleaner.ts"],"names":[],"mappings":";;;AACA,+HAAyG;AACzG,6DAA2D;AAQpD,MAAM,8BAA8B,GAAG,CAC5C,MAA4C,EACtC,EAAE;IACR,MAAM,EAAE,mBAAmB,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,CAAC;IACvD,IAAI,CAAC,mBAAmB,IAAI,CAAC,MAAM,EAAE,CAAC;QACpC,OAAO;IACT,CAAC;IACD,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,MAAM,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,SAAS,CAAC,QAAQ,CAAC,EAAE,CAAC;QACrE,IAAA,wCAAmB,EAAC;YAClB,OAAO,EAAE,mBAAmB;YAC5B,WAAW,EAAE,MAAM;YACnB,WAAW,EAAE,IAAA,wDAAiB,EAAC,KAAK,CAAC,GAAG,CAAC;SAC1C,CAAC,CAAC;IACL,CAAC;AACH,CAAC,CAAC;AAdW,QAAA,8BAA8B,kCAczC"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.ownerCallProjectCodeInInTmuxByHumanData = exports.ownerCallFileDeleteInEveryProject = exports.ownerCallFileDelete = exports.ownerCallFileAppend = exports.ownerCallFilePath = void 0;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const OwnerCallFile_1 = require("../../../domain/usecases/intmux/OwnerCallFile");
|
|
10
|
+
const ownerCallFilePath = (dataDir, projectCode, sessionName) => path_1.default.join(dataDir, (0, OwnerCallFile_1.ownerCallFileRelativePath)(projectCode, sessionName));
|
|
11
|
+
exports.ownerCallFilePath = ownerCallFilePath;
|
|
12
|
+
const ownerCallFileAppend = (params) => {
|
|
13
|
+
const filePath = (0, exports.ownerCallFilePath)(params.dataDir, params.projectCode, params.ownerCall.sessionName);
|
|
14
|
+
fs_1.default.mkdirSync(path_1.default.dirname(filePath), { recursive: true });
|
|
15
|
+
fs_1.default.appendFileSync(filePath, (0, OwnerCallFile_1.ownerCallYamlDocument)(params.ownerCall));
|
|
16
|
+
};
|
|
17
|
+
exports.ownerCallFileAppend = ownerCallFileAppend;
|
|
18
|
+
const ownerCallFileDelete = (params) => {
|
|
19
|
+
fs_1.default.rmSync((0, exports.ownerCallFilePath)(params.dataDir, params.projectCode, params.sessionName), { force: true });
|
|
20
|
+
};
|
|
21
|
+
exports.ownerCallFileDelete = ownerCallFileDelete;
|
|
22
|
+
const projectCodeDirectoryNames = (dataDir) => {
|
|
23
|
+
const ownerCallDirectory = path_1.default.join(dataDir, OwnerCallFile_1.OWNER_CALL_FILE_DIRECTORY_NAME);
|
|
24
|
+
if (!fs_1.default.existsSync(ownerCallDirectory)) {
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
return fs_1.default
|
|
28
|
+
.readdirSync(ownerCallDirectory, { withFileTypes: true })
|
|
29
|
+
.filter((entry) => entry.isDirectory())
|
|
30
|
+
.map((entry) => entry.name);
|
|
31
|
+
};
|
|
32
|
+
const ownerCallFileDeleteInEveryProject = (params) => {
|
|
33
|
+
for (const projectCode of projectCodeDirectoryNames(params.dataDir)) {
|
|
34
|
+
(0, exports.ownerCallFileDelete)({
|
|
35
|
+
dataDir: params.dataDir,
|
|
36
|
+
projectCode,
|
|
37
|
+
sessionName: params.sessionName,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
exports.ownerCallFileDeleteInEveryProject = ownerCallFileDeleteInEveryProject;
|
|
42
|
+
// The in-tmux-by-human data the scheduled run writes into the same directory
|
|
43
|
+
// serveWeb serves: one `{projectCode}.v4.json` per project, each listing the
|
|
44
|
+
// sessions of that project. `index.v4.json` only names the project files, so
|
|
45
|
+
// it is not read here.
|
|
46
|
+
const IN_TMUX_BY_HUMAN_PROJECT_DATA_SUFFIX = '.v4.json';
|
|
47
|
+
const IN_TMUX_BY_HUMAN_INDEX_FILE_NAME = `index${IN_TMUX_BY_HUMAN_PROJECT_DATA_SUFFIX}`;
|
|
48
|
+
const isUnknownArray = (value) => Array.isArray(value);
|
|
49
|
+
const isRecord = (value) => typeof value === 'object' && value !== null;
|
|
50
|
+
const arrayOrEmpty = (value) => isUnknownArray(value) ? value : [];
|
|
51
|
+
const propertyOrUndefined = (value, key) => isRecord(value) ? value[key] : undefined;
|
|
52
|
+
const sessionNamesInProjectDataFile = (filePath) => {
|
|
53
|
+
let parsed;
|
|
54
|
+
try {
|
|
55
|
+
parsed = JSON.parse(fs_1.default.readFileSync(filePath, 'utf-8'));
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
60
|
+
return arrayOrEmpty(propertyOrUndefined(parsed, 'groups')).flatMap((group) => arrayOrEmpty(propertyOrUndefined(group, 'sessions'))
|
|
61
|
+
.map((session) => propertyOrUndefined(session, 'name'))
|
|
62
|
+
.filter((name) => typeof name === 'string'));
|
|
63
|
+
};
|
|
64
|
+
const inTmuxByHumanProjectSessionNames = (dataDir) => {
|
|
65
|
+
if (!fs_1.default.existsSync(dataDir)) {
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
68
|
+
return fs_1.default
|
|
69
|
+
.readdirSync(dataDir, { withFileTypes: true })
|
|
70
|
+
.filter((entry) => entry.isFile() &&
|
|
71
|
+
entry.name.endsWith(IN_TMUX_BY_HUMAN_PROJECT_DATA_SUFFIX) &&
|
|
72
|
+
entry.name !== IN_TMUX_BY_HUMAN_INDEX_FILE_NAME)
|
|
73
|
+
.map((entry) => ({
|
|
74
|
+
projectCode: entry.name.slice(0, entry.name.length - IN_TMUX_BY_HUMAN_PROJECT_DATA_SUFFIX.length),
|
|
75
|
+
sessionNames: sessionNamesInProjectDataFile(path_1.default.join(dataDir, entry.name)),
|
|
76
|
+
}));
|
|
77
|
+
};
|
|
78
|
+
const ownerCallProjectCodeInInTmuxByHumanData = (dataDir, sessionName) => (0, OwnerCallFile_1.ownerCallProjectCodeOfSession)(inTmuxByHumanProjectSessionNames(dataDir), sessionName);
|
|
79
|
+
exports.ownerCallProjectCodeInInTmuxByHumanData = ownerCallProjectCodeInInTmuxByHumanData;
|
|
80
|
+
//# sourceMappingURL=ownerCallFileStore.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ownerCallFileStore.js","sourceRoot":"","sources":["../../../../src/adapter/entry-points/handlers/ownerCallFileStore.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ownerCallYamlDocument = exports.isOwnerCallCalledAtValid = exports.ownerCallProjectCodeOfSession = exports.ownerCallFileRelativePath = exports.ownerCallFileSessionKey = exports.OWNER_CALL_FILE_EXTENSION = exports.OWNER_CALL_FILE_PROJECT_CODE_FOR_NO_PROJECT = exports.OWNER_CALL_FILE_DIRECTORY_NAME = void 0;
|
|
4
|
+
const InTmuxByHumanSessionReconcileUseCase_1 = require("./InTmuxByHumanSessionReconcileUseCase");
|
|
5
|
+
exports.OWNER_CALL_FILE_DIRECTORY_NAME = 'call-to-user';
|
|
6
|
+
exports.OWNER_CALL_FILE_PROJECT_CODE_FOR_NO_PROJECT = 'NA';
|
|
7
|
+
exports.OWNER_CALL_FILE_EXTENSION = '.yaml';
|
|
8
|
+
// The key one owner call file is named by. It is the tmux session name
|
|
9
|
+
// derivation every other in-tmux caller uses, followed by the replacement of
|
|
10
|
+
// the slashes tmux keeps but a file name cannot hold. Applying it to a name
|
|
11
|
+
// that already went through it gives that same name back, so the writing side,
|
|
12
|
+
// which knows the tmux session name, and the reading side, which knows the
|
|
13
|
+
// issue url, reach the same file.
|
|
14
|
+
const ownerCallFileSessionKey = (sessionName) => (0, InTmuxByHumanSessionReconcileUseCase_1.toTmuxSessionName)(sessionName).replace(/\//g, '_');
|
|
15
|
+
exports.ownerCallFileSessionKey = ownerCallFileSessionKey;
|
|
16
|
+
const ownerCallFileRelativePath = (projectCode, sessionName) => {
|
|
17
|
+
const directory = projectCode ?? exports.OWNER_CALL_FILE_PROJECT_CODE_FOR_NO_PROJECT;
|
|
18
|
+
const fileName = (0, exports.ownerCallFileSessionKey)(sessionName);
|
|
19
|
+
return `${exports.OWNER_CALL_FILE_DIRECTORY_NAME}/${directory}/${fileName}${exports.OWNER_CALL_FILE_EXTENSION}`;
|
|
20
|
+
};
|
|
21
|
+
exports.ownerCallFileRelativePath = ownerCallFileRelativePath;
|
|
22
|
+
const ownerCallProjectCodeOfSession = (projects, sessionName) => {
|
|
23
|
+
const key = (0, exports.ownerCallFileSessionKey)(sessionName);
|
|
24
|
+
const owningProject = projects.find((project) => project.sessionNames.some((candidate) => (0, exports.ownerCallFileSessionKey)(candidate) === key));
|
|
25
|
+
return owningProject ? owningProject.projectCode : null;
|
|
26
|
+
};
|
|
27
|
+
exports.ownerCallProjectCodeOfSession = ownerCallProjectCodeOfSession;
|
|
28
|
+
const CALLED_AT_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/;
|
|
29
|
+
const isOwnerCallCalledAtValid = (calledAt) => {
|
|
30
|
+
if (!CALLED_AT_PATTERN.test(calledAt)) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
const parsed = new Date(calledAt);
|
|
34
|
+
return (Number.isFinite(parsed.getTime()) &&
|
|
35
|
+
parsed.toISOString().replace(/\.\d{3}Z$/, 'Z') === calledAt);
|
|
36
|
+
};
|
|
37
|
+
exports.isOwnerCallCalledAtValid = isOwnerCallCalledAtValid;
|
|
38
|
+
const BODY_INDENTATION = ' ';
|
|
39
|
+
const bodyBlockLines = (body) => body
|
|
40
|
+
.replace(/\n+$/, '')
|
|
41
|
+
.split('\n')
|
|
42
|
+
.map((line) => (line.length === 0 ? '' : `${BODY_INDENTATION}${line}`));
|
|
43
|
+
const ownerCallYamlDocument = (ownerCall) => [
|
|
44
|
+
'---',
|
|
45
|
+
`sessionName: ${JSON.stringify(ownerCall.sessionName)}`,
|
|
46
|
+
`calledAt: ${JSON.stringify(ownerCall.calledAt)}`,
|
|
47
|
+
`body: |${BODY_INDENTATION.length}`,
|
|
48
|
+
...bodyBlockLines(ownerCall.body),
|
|
49
|
+
'',
|
|
50
|
+
].join('\n');
|
|
51
|
+
exports.ownerCallYamlDocument = ownerCallYamlDocument;
|
|
52
|
+
//# sourceMappingURL=OwnerCallFile.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"OwnerCallFile.js","sourceRoot":"","sources":["../../../../src/domain/usecases/intmux/OwnerCallFile.ts"],"names":[],"mappings":";;;AAAA,iGAA2E;AAE9D,QAAA,8BAA8B,GAAG,cAAc,CAAC;AAEhD,QAAA,2CAA2C,GAAG,IAAI,CAAC;AAEnD,QAAA,yBAAyB,GAAG,OAAO,CAAC;AAQjD,uEAAuE;AACvE,6EAA6E;AAC7E,4EAA4E;AAC5E,+EAA+E;AAC/E,2EAA2E;AAC3E,kCAAkC;AAC3B,MAAM,uBAAuB,GAAG,CAAC,WAAmB,EAAU,EAAE,CACrE,IAAA,wDAAiB,EAAC,WAAW,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;AADxC,QAAA,uBAAuB,2BACiB;AAE9C,MAAM,yBAAyB,GAAG,CACvC,WAA0B,EAC1B,WAAmB,EACX,EAAE;IACV,MAAM,SAAS,GAAG,WAAW,IAAI,mDAA2C,CAAC;IAC7E,MAAM,QAAQ,GAAG,IAAA,+BAAuB,EAAC,WAAW,CAAC,CAAC;IACtD,OAAO,GAAG,sCAA8B,IAAI,SAAS,IAAI,QAAQ,GAAG,iCAAyB,EAAE,CAAC;AAClG,CAAC,CAAC;AAPW,QAAA,yBAAyB,6BAOpC;AAOK,MAAM,6BAA6B,GAAG,CAC3C,QAAwC,EACxC,WAAmB,EACJ,EAAE;IACjB,MAAM,GAAG,GAAG,IAAA,+BAAuB,EAAC,WAAW,CAAC,CAAC;IACjD,MAAM,aAAa,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAC9C,OAAO,CAAC,YAAY,CAAC,IAAI,CACvB,CAAC,SAAS,EAAE,EAAE,CAAC,IAAA,+BAAuB,EAAC,SAAS,CAAC,KAAK,GAAG,CAC1D,CACF,CAAC;IACF,OAAO,aAAa,CAAC,CAAC,CAAC,aAAa,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC;AAC1D,CAAC,CAAC;AAXW,QAAA,6BAA6B,iCAWxC;AAEF,MAAM,iBAAiB,GAAG,wCAAwC,CAAC;AAE5D,MAAM,wBAAwB,GAAG,CAAC,QAAgB,EAAW,EAAE;IACpE,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;QACtC,OAAO,KAAK,CAAC;IACf,CAAC;IACD,MAAM,MAAM,GAAG,IAAI,IAAI,CAAC,QAAQ,CAAC,CAAC;IAClC,OAAO,CACL,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;QACjC,MAAM,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,WAAW,EAAE,GAAG,CAAC,KAAK,QAAQ,CAC5D,CAAC;AACJ,CAAC,CAAC;AATW,QAAA,wBAAwB,4BASnC;AAEF,MAAM,gBAAgB,GAAG,IAAI,CAAC;AAE9B,MAAM,cAAc,GAAG,CAAC,IAAY,EAAY,EAAE,CAChD,IAAI;KACD,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC;KACnB,KAAK,CAAC,IAAI,CAAC;KACX,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,gBAAgB,GAAG,IAAI,EAAE,CAAC,CAAC,CAAC;AAErE,MAAM,qBAAqB,GAAG,CAAC,SAAoB,EAAU,EAAE,CACpE;IACE,KAAK;IACL,gBAAgB,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,WAAW,CAAC,EAAE;IACvD,aAAa,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,QAAQ,CAAC,EAAE;IACjD,UAAU,gBAAgB,CAAC,MAAM,EAAE;IACnC,GAAG,cAAc,CAAC,SAAS,CAAC,IAAI,CAAC;IACjC,EAAE;CACH,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AARF,QAAA,qBAAqB,yBAQnB"}
|
package/package.json
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
|
+
import * as os from 'os';
|
|
2
3
|
import * as path from 'path';
|
|
3
4
|
import YAML from 'yaml';
|
|
4
5
|
|
|
@@ -21,6 +22,8 @@ import {
|
|
|
21
22
|
import { StartPreparationUseCase } from '../../../domain/usecases/StartPreparationUseCase';
|
|
22
23
|
import { NotifyFinishedIssuePreparationUseCase } from '../../../domain/usecases/NotifyFinishedIssuePreparationUseCase';
|
|
23
24
|
import { CheckIssueReviewReadinessUseCase } from '../../../domain/usecases/CheckIssueReviewReadinessUseCase';
|
|
25
|
+
import { ownerCallFileRelativePath } from '../../../domain/usecases/intmux/OwnerCallFile';
|
|
26
|
+
import { toTmuxSessionName } from '../../../domain/usecases/intmux/InTmuxByHumanSessionReconcileUseCase';
|
|
24
27
|
|
|
25
28
|
jest.mock('../../../domain/usecases/StartPreparationUseCase');
|
|
26
29
|
jest.mock('../../../domain/usecases/NotifyFinishedIssuePreparationUseCase');
|
|
@@ -2371,6 +2374,192 @@ mysteryKey: 'value'
|
|
|
2371
2374
|
});
|
|
2372
2375
|
});
|
|
2373
2376
|
|
|
2377
|
+
describe('ownerCallFileAppend and ownerCallFileDelete', () => {
|
|
2378
|
+
const issueUrl = 'https://github.com/OWNER/REPO/issues/1';
|
|
2379
|
+
const otherIssueUrl = 'https://github.com/OWNER/REPO/issues/2';
|
|
2380
|
+
const sessionName = toTmuxSessionName(issueUrl);
|
|
2381
|
+
let ownerCallDataDir = '';
|
|
2382
|
+
let bodyFilePath = '';
|
|
2383
|
+
|
|
2384
|
+
beforeEach(() => {
|
|
2385
|
+
ownerCallDataDir = fs.mkdtempSync(
|
|
2386
|
+
path.join(os.tmpdir(), 'owner-call-cli-'),
|
|
2387
|
+
);
|
|
2388
|
+
bodyFilePath = path.join(ownerCallDataDir, 'body.txt');
|
|
2389
|
+
fs.writeFileSync(bodyFilePath, ' the call body\n\nsecond paragraph\n');
|
|
2390
|
+
});
|
|
2391
|
+
|
|
2392
|
+
afterEach(() => {
|
|
2393
|
+
fs.rmSync(ownerCallDataDir, { recursive: true, force: true });
|
|
2394
|
+
});
|
|
2395
|
+
|
|
2396
|
+
// The in-tmux-by-human data the scheduled run writes into the same
|
|
2397
|
+
// directory serveWeb serves: one file per project, each listing the
|
|
2398
|
+
// sessions of that project by their raw issue url.
|
|
2399
|
+
type InTmuxByHumanProjectData = {
|
|
2400
|
+
projectCode: string;
|
|
2401
|
+
sessionIssueUrls: string[];
|
|
2402
|
+
};
|
|
2403
|
+
|
|
2404
|
+
const writeInTmuxByHumanData = (
|
|
2405
|
+
projects: InTmuxByHumanProjectData[],
|
|
2406
|
+
): void => {
|
|
2407
|
+
for (const project of projects) {
|
|
2408
|
+
fs.writeFileSync(
|
|
2409
|
+
path.join(ownerCallDataDir, `${project.projectCode}.v4.json`),
|
|
2410
|
+
`${JSON.stringify({
|
|
2411
|
+
version: 4,
|
|
2412
|
+
overviewUrl: 'https://github.com/orgs/OWNER/projects/1',
|
|
2413
|
+
tdpmConsoleUrl: 'http://localhost/projects/code',
|
|
2414
|
+
newIssueUrl: 'https://github.com/OWNER/REPO/issues/new',
|
|
2415
|
+
groups: [
|
|
2416
|
+
{
|
|
2417
|
+
story: 'a story',
|
|
2418
|
+
sessions: project.sessionIssueUrls.map((url) => ({
|
|
2419
|
+
name: url,
|
|
2420
|
+
description: 'an issue title',
|
|
2421
|
+
})),
|
|
2422
|
+
},
|
|
2423
|
+
],
|
|
2424
|
+
})}\n`,
|
|
2425
|
+
);
|
|
2426
|
+
}
|
|
2427
|
+
};
|
|
2428
|
+
|
|
2429
|
+
const projectCodeListing = (
|
|
2430
|
+
projects: InTmuxByHumanProjectData[],
|
|
2431
|
+
url: string,
|
|
2432
|
+
): string | null =>
|
|
2433
|
+
projects.find((project) => project.sessionIssueUrls.includes(url))
|
|
2434
|
+
?.projectCode ?? null;
|
|
2435
|
+
|
|
2436
|
+
const appendedFilePath = (projectCode: string | null): string =>
|
|
2437
|
+
path.join(
|
|
2438
|
+
ownerCallDataDir,
|
|
2439
|
+
ownerCallFileRelativePath(projectCode, sessionName),
|
|
2440
|
+
);
|
|
2441
|
+
|
|
2442
|
+
const runAppend = (calledAt: string): Promise<unknown> =>
|
|
2443
|
+
program.parseAsync([
|
|
2444
|
+
'node',
|
|
2445
|
+
'test',
|
|
2446
|
+
'ownerCallFileAppend',
|
|
2447
|
+
'--session',
|
|
2448
|
+
sessionName,
|
|
2449
|
+
'--calledAt',
|
|
2450
|
+
calledAt,
|
|
2451
|
+
'--body-file',
|
|
2452
|
+
bodyFilePath,
|
|
2453
|
+
'--inTmuxDataDir',
|
|
2454
|
+
ownerCallDataDir,
|
|
2455
|
+
]);
|
|
2456
|
+
|
|
2457
|
+
it('appends the call under the directory of the project whose session list holds the session', async () => {
|
|
2458
|
+
const projects: InTmuxByHumanProjectData[] = [
|
|
2459
|
+
{ projectCode: 'other', sessionIssueUrls: [otherIssueUrl] },
|
|
2460
|
+
{ projectCode: 'umino', sessionIssueUrls: [issueUrl] },
|
|
2461
|
+
];
|
|
2462
|
+
writeInTmuxByHumanData(projects);
|
|
2463
|
+
const stdoutSpy = jest
|
|
2464
|
+
.spyOn(process.stdout, 'write')
|
|
2465
|
+
.mockImplementation(() => true);
|
|
2466
|
+
|
|
2467
|
+
await runAppend('2026-08-14T04:22:28Z');
|
|
2468
|
+
|
|
2469
|
+
const documents = YAML.parseAllDocuments(
|
|
2470
|
+
fs.readFileSync(
|
|
2471
|
+
appendedFilePath(projectCodeListing(projects, issueUrl)),
|
|
2472
|
+
'utf-8',
|
|
2473
|
+
),
|
|
2474
|
+
);
|
|
2475
|
+
expect(documents).toHaveLength(1);
|
|
2476
|
+
expect(documents[0].toJS()).toEqual({
|
|
2477
|
+
sessionName,
|
|
2478
|
+
calledAt: '2026-08-14T04:22:28Z',
|
|
2479
|
+
body: ' the call body\n\nsecond paragraph\n',
|
|
2480
|
+
});
|
|
2481
|
+
expect(stdoutSpy).not.toHaveBeenCalled();
|
|
2482
|
+
|
|
2483
|
+
stdoutSpy.mockRestore();
|
|
2484
|
+
});
|
|
2485
|
+
|
|
2486
|
+
it('appends the call under NA when no project lists the session', async () => {
|
|
2487
|
+
const projects: InTmuxByHumanProjectData[] = [
|
|
2488
|
+
{ projectCode: 'umino', sessionIssueUrls: [otherIssueUrl] },
|
|
2489
|
+
];
|
|
2490
|
+
writeInTmuxByHumanData(projects);
|
|
2491
|
+
|
|
2492
|
+
await runAppend('2026-08-14T04:22:28Z');
|
|
2493
|
+
|
|
2494
|
+
expect(projectCodeListing(projects, issueUrl)).toBeNull();
|
|
2495
|
+
expect(fs.existsSync(appendedFilePath(null))).toBe(true);
|
|
2496
|
+
});
|
|
2497
|
+
|
|
2498
|
+
it('rejects a calledAt that is not a UTC ISO-8601 timestamp with second precision', async () => {
|
|
2499
|
+
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
|
|
2500
|
+
const processExitSpy = jest
|
|
2501
|
+
.spyOn(process, 'exit')
|
|
2502
|
+
.mockImplementation(() => {
|
|
2503
|
+
throw new Error('process.exit called');
|
|
2504
|
+
});
|
|
2505
|
+
|
|
2506
|
+
await expect(runAppend('2026-08-14 04:22:28')).rejects.toThrow(
|
|
2507
|
+
'process.exit called',
|
|
2508
|
+
);
|
|
2509
|
+
expect(processExitSpy).toHaveBeenCalledWith(1);
|
|
2510
|
+
expect(fs.existsSync(appendedFilePath(null))).toBe(false);
|
|
2511
|
+
|
|
2512
|
+
consoleErrorSpy.mockRestore();
|
|
2513
|
+
processExitSpy.mockRestore();
|
|
2514
|
+
});
|
|
2515
|
+
|
|
2516
|
+
const runDelete = (): Promise<unknown> =>
|
|
2517
|
+
program.parseAsync([
|
|
2518
|
+
'node',
|
|
2519
|
+
'test',
|
|
2520
|
+
'ownerCallFileDelete',
|
|
2521
|
+
'--session',
|
|
2522
|
+
sessionName,
|
|
2523
|
+
'--inTmuxDataDir',
|
|
2524
|
+
ownerCallDataDir,
|
|
2525
|
+
]);
|
|
2526
|
+
|
|
2527
|
+
it('deletes the file of the session whichever project directory holds it', async () => {
|
|
2528
|
+
const projects: InTmuxByHumanProjectData[] = [
|
|
2529
|
+
{ projectCode: 'umino', sessionIssueUrls: [issueUrl] },
|
|
2530
|
+
];
|
|
2531
|
+
writeInTmuxByHumanData(projects);
|
|
2532
|
+
await runAppend('2026-08-14T04:22:28Z');
|
|
2533
|
+
const writtenFilePath = appendedFilePath(
|
|
2534
|
+
projectCodeListing(projects, issueUrl),
|
|
2535
|
+
);
|
|
2536
|
+
expect(fs.existsSync(writtenFilePath)).toBe(true);
|
|
2537
|
+
|
|
2538
|
+
await runDelete();
|
|
2539
|
+
|
|
2540
|
+
expect(fs.existsSync(writtenFilePath)).toBe(false);
|
|
2541
|
+
});
|
|
2542
|
+
|
|
2543
|
+
it('deletes the file left behind under the project the session has since left', async () => {
|
|
2544
|
+
writeInTmuxByHumanData([
|
|
2545
|
+
{ projectCode: 'umino', sessionIssueUrls: [issueUrl] },
|
|
2546
|
+
]);
|
|
2547
|
+
await runAppend('2026-08-14T04:22:28Z');
|
|
2548
|
+
writeInTmuxByHumanData([
|
|
2549
|
+
{ projectCode: 'umino', sessionIssueUrls: [] },
|
|
2550
|
+
{ projectCode: 'other', sessionIssueUrls: [issueUrl] },
|
|
2551
|
+
]);
|
|
2552
|
+
|
|
2553
|
+
await runDelete();
|
|
2554
|
+
|
|
2555
|
+
expect(fs.existsSync(appendedFilePath('umino'))).toBe(false);
|
|
2556
|
+
});
|
|
2557
|
+
|
|
2558
|
+
it('succeeds when the file to delete is already absent', async () => {
|
|
2559
|
+
await expect(runDelete()).resolves.toBeDefined();
|
|
2560
|
+
});
|
|
2561
|
+
});
|
|
2562
|
+
|
|
2374
2563
|
describe('runCliProgram', () => {
|
|
2375
2564
|
const mockCheckUseCaseRejectingWith = (error: Error): void => {
|
|
2376
2565
|
const MockedCheckUseCase = jest.mocked(CheckIssueReviewReadinessUseCase);
|
|
@@ -63,6 +63,12 @@ import {
|
|
|
63
63
|
loadLiveSessionOauthTokenSelectionSettings,
|
|
64
64
|
resolveFleetConfigFilePath,
|
|
65
65
|
} from './fleetConfig';
|
|
66
|
+
import { isOwnerCallCalledAtValid } from '../../../domain/usecases/intmux/OwnerCallFile';
|
|
67
|
+
import {
|
|
68
|
+
ownerCallFileAppend,
|
|
69
|
+
ownerCallFileDeleteInEveryProject,
|
|
70
|
+
ownerCallProjectCodeInInTmuxByHumanData,
|
|
71
|
+
} from '../handlers/ownerCallFileStore';
|
|
66
72
|
|
|
67
73
|
type StartDaemonOptions = {
|
|
68
74
|
projectUrl?: string;
|
|
@@ -102,6 +108,18 @@ type ServeWebOptions = {
|
|
|
102
108
|
dashboardProjectNames?: string;
|
|
103
109
|
};
|
|
104
110
|
|
|
111
|
+
type OwnerCallFileAppendOptions = {
|
|
112
|
+
session: string;
|
|
113
|
+
calledAt: string;
|
|
114
|
+
bodyFile: string;
|
|
115
|
+
inTmuxDataDir?: string;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
type OwnerCallFileDeleteOptions = {
|
|
119
|
+
session: string;
|
|
120
|
+
inTmuxDataDir?: string;
|
|
121
|
+
};
|
|
122
|
+
|
|
105
123
|
const DEFAULT_IN_TMUX_DATA_DIR =
|
|
106
124
|
'/home/hiromi/0_workspaces/workspace1/jsonpub/in-tmux-by-human';
|
|
107
125
|
|
|
@@ -1149,6 +1167,63 @@ program
|
|
|
1149
1167
|
}
|
|
1150
1168
|
});
|
|
1151
1169
|
|
|
1170
|
+
program
|
|
1171
|
+
.command('ownerCallFileAppend')
|
|
1172
|
+
.description(
|
|
1173
|
+
'Append one owner call as a YAML document to the per-session owner call file. The project the session belongs to is resolved from the in-tmux-by-human data in the data directory, and a session no project lists is written under NA. The file is created when it does not exist, and every later call is appended after the existing documents so the oldest call stays first. Nothing is written to stdout on success.',
|
|
1174
|
+
)
|
|
1175
|
+
.requiredOption('--session <name>', 'tmux session name that raised the call')
|
|
1176
|
+
.requiredOption(
|
|
1177
|
+
'--calledAt <timestamp>',
|
|
1178
|
+
'Time the call was raised, as UTC ISO-8601 with second precision and a trailing Z',
|
|
1179
|
+
)
|
|
1180
|
+
.requiredOption(
|
|
1181
|
+
'--body-file <path>',
|
|
1182
|
+
'Path to the file holding the call body; the body is read from a file because it is multi-line and can be long',
|
|
1183
|
+
)
|
|
1184
|
+
.option(
|
|
1185
|
+
'--inTmuxDataDir <path>',
|
|
1186
|
+
`Directory the owner call files are written under, the same directory serveWeb serves them from (default: ${DEFAULT_IN_TMUX_DATA_DIR})`,
|
|
1187
|
+
)
|
|
1188
|
+
.action((options: OwnerCallFileAppendOptions) => {
|
|
1189
|
+
if (!isOwnerCallCalledAtValid(options.calledAt)) {
|
|
1190
|
+
console.error(
|
|
1191
|
+
'--calledAt must be a UTC ISO-8601 timestamp with second precision and a trailing Z, for example 2026-08-14T04:22:28Z',
|
|
1192
|
+
);
|
|
1193
|
+
return process.exit(1);
|
|
1194
|
+
}
|
|
1195
|
+
const dataDir = options.inTmuxDataDir ?? DEFAULT_IN_TMUX_DATA_DIR;
|
|
1196
|
+
ownerCallFileAppend({
|
|
1197
|
+
dataDir,
|
|
1198
|
+
projectCode: ownerCallProjectCodeInInTmuxByHumanData(
|
|
1199
|
+
dataDir,
|
|
1200
|
+
options.session,
|
|
1201
|
+
),
|
|
1202
|
+
ownerCall: {
|
|
1203
|
+
sessionName: options.session,
|
|
1204
|
+
calledAt: options.calledAt,
|
|
1205
|
+
body: fs.readFileSync(options.bodyFile, 'utf-8'),
|
|
1206
|
+
},
|
|
1207
|
+
});
|
|
1208
|
+
});
|
|
1209
|
+
|
|
1210
|
+
program
|
|
1211
|
+
.command('ownerCallFileDelete')
|
|
1212
|
+
.description(
|
|
1213
|
+
'Delete the owner call file of one session, whichever project directory holds it, so a session that moved between projects after its call was appended leaves nothing behind. It succeeds when the file is already absent, so a reset that runs twice is not an error. Nothing is written to stdout on success.',
|
|
1214
|
+
)
|
|
1215
|
+
.requiredOption('--session <name>', 'tmux session name that raised the call')
|
|
1216
|
+
.option(
|
|
1217
|
+
'--inTmuxDataDir <path>',
|
|
1218
|
+
`Directory the owner call files are written under, the same directory serveWeb serves them from (default: ${DEFAULT_IN_TMUX_DATA_DIR})`,
|
|
1219
|
+
)
|
|
1220
|
+
.action((options: OwnerCallFileDeleteOptions) => {
|
|
1221
|
+
ownerCallFileDeleteInEveryProject({
|
|
1222
|
+
dataDir: options.inTmuxDataDir ?? DEFAULT_IN_TMUX_DATA_DIR,
|
|
1223
|
+
sessionName: options.session,
|
|
1224
|
+
});
|
|
1225
|
+
});
|
|
1226
|
+
|
|
1152
1227
|
export const reportFatalErrorAndExit = (error: unknown): void => {
|
|
1153
1228
|
console.error(error);
|
|
1154
1229
|
process.exit(1);
|